/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: 2020-03-22 01:35:14 UTC
  • mfrom: (7490.7.6 work)
  • mto: This revision was merged to the branch mainline in revision 7499.
  • Revision ID: jelmer@jelmer.uk-20200322013514-7vw1ntwho04rcuj3
merge lp:brz/3.1.

Show diffs side-by-side

added added

removed removed

Lines of Context:
47
47
all the changes since the previous revision that touched hello.c.
48
48
"""
49
49
 
50
 
from __future__ import absolute_import
51
 
 
52
50
import codecs
 
51
from io import BytesIO
53
52
import itertools
54
53
import re
55
54
import sys
64
63
    config,
65
64
    controldir,
66
65
    diff,
67
 
    errors,
68
66
    foreign,
69
 
    repository as _mod_repository,
 
67
    lazy_regex,
70
68
    revision as _mod_revision,
71
 
    tsort,
72
69
    )
73
70
from breezy.i18n import gettext, ngettext
74
71
""")
75
72
 
76
73
from . import (
77
 
    lazy_regex,
 
74
    errors,
78
75
    registry,
79
76
    revisionspec,
 
77
    trace,
80
78
    )
81
79
from .osutils import (
82
80
    format_date,
85
83
    get_terminal_encoding,
86
84
    terminal_width,
87
85
    )
88
 
from breezy.sixish import (
89
 
    BytesIO,
90
 
    range,
91
 
    zip,
 
86
from .tree import (
 
87
    find_previous_path,
 
88
    InterTree,
92
89
    )
93
90
 
94
91
 
95
 
def find_touching_revisions(branch, file_id):
 
92
def find_touching_revisions(repository, last_revision, last_tree, last_path):
96
93
    """Yield a description of revisions which affect the file_id.
97
94
 
98
95
    Each returned element is (revno, revision_id, description)
103
100
    TODO: Perhaps some way to limit this to only particular revisions,
104
101
    or to traverse a non-mainline set of revisions?
105
102
    """
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)
 
103
    last_verifier = last_tree.get_file_verifier(last_path)
 
104
    graph = repository.get_graph()
 
105
    history = list(graph.iter_lefthand_ancestry(last_revision, []))
 
106
    revno = len(history)
 
107
    for revision_id in history:
 
108
        this_tree = repository.revision_tree(revision_id)
 
109
        this_intertree = InterTree.get(this_tree, last_tree)
 
110
        this_path = this_intertree.find_source_path(last_path)
120
111
 
121
112
        # now we know how it was last time, and how it is in this revision.
122
113
        # are those two states effectively the same or not?
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
 
114
        if this_path is not None and last_path is None:
 
115
            yield revno, revision_id, "deleted " + this_path
 
116
            this_verifier = this_tree.get_file_verifier(this_path)
 
117
        elif this_path is None and last_path is not None:
 
118
            yield revno, revision_id, "added " + last_path
132
119
        elif this_path != last_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
 
120
            yield revno, revision_id, ("renamed %s => %s" % (this_path, last_path))
 
121
            this_verifier = this_tree.get_file_verifier(this_path)
 
122
        else:
 
123
            this_verifier = this_tree.get_file_verifier(this_path)
 
124
            if (this_verifier != last_verifier):
 
125
                yield revno, revision_id, "modified " + this_path
136
126
 
137
127
        last_verifier = this_verifier
138
128
        last_path = this_path
139
 
        revno += 1
 
129
        last_tree = this_tree
 
130
        if last_path is None:
 
131
            return
 
132
        revno -= 1
140
133
 
141
134
 
142
135
def show_log(branch,
143
136
             lf,
144
 
             specific_fileid=None,
145
137
             verbose=False,
146
138
             direction='reverse',
147
139
             start_revision=None,
159
151
 
160
152
    :param lf: The LogFormatter object showing the output.
161
153
 
162
 
    :param specific_fileid: If not None, list only the commits affecting the
163
 
        specified file, rather than all commits.
164
 
 
165
154
    :param verbose: If True show added/changed/deleted/renamed files.
166
155
 
167
156
    :param direction: 'reverse' (default) is latest to earliest; 'forward' is
182
171
    :param match: Dictionary of search lists to use when matching revision
183
172
      properties.
184
173
    """
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
190
174
    if verbose:
191
 
        if file_ids:
192
 
            delta_type = 'partial'
193
 
        else:
194
 
            delta_type = 'full'
 
175
        delta_type = 'full'
195
176
    else:
196
177
        delta_type = None
197
178
    if show_diff:
198
 
        if file_ids:
199
 
            diff_type = 'partial'
200
 
        else:
201
 
            diff_type = 'full'
 
179
        diff_type = 'full'
202
180
    else:
203
181
        diff_type = None
204
182
 
 
183
    if isinstance(start_revision, int):
 
184
        try:
 
185
            start_revision = revisionspec.RevisionInfo(branch, start_revision)
 
186
        except (errors.NoSuchRevision, errors.RevnoOutOfBounds):
 
187
            raise errors.InvalidRevisionNumber(start_revision)
 
188
 
 
189
    if isinstance(end_revision, int):
 
190
        try:
 
191
            end_revision = revisionspec.RevisionInfo(branch, end_revision)
 
192
        except (errors.NoSuchRevision, errors.RevnoOutOfBounds):
 
193
            raise errors.InvalidRevisionNumber(end_revision)
 
194
 
 
195
    if end_revision is not None and end_revision.revno == 0:
 
196
        raise errors.InvalidRevisionNumber(end_revision.revno)
 
197
 
205
198
    # Build the request and execute it
206
 
    rqst = make_log_request_dict(direction=direction, specific_fileids=file_ids,
 
199
    rqst = make_log_request_dict(
 
200
        direction=direction,
207
201
        start_revision=start_revision, end_revision=end_revision,
208
202
        limit=limit, message_search=search,
209
203
        delta_type=delta_type, diff_type=diff_type)
295
289
            else:
296
290
                match['message'] = [message_search]
297
291
        else:
298
 
            match={ 'message': [message_search] }
 
292
            match = {'message': [message_search]}
299
293
    return {
300
294
        'direction': direction,
301
295
        'specific_fileids': specific_fileids,
376
370
        if not isinstance(lf, LogFormatter):
377
371
            warn("not a LogFormatter instance: %r" % lf)
378
372
 
379
 
        self.branch.lock_read()
380
 
        try:
 
373
        with self.branch.lock_read():
381
374
            if getattr(lf, 'begin_log', None):
382
375
                lf.begin_log()
383
376
            self._show_body(lf)
384
377
            if getattr(lf, 'end_log', None):
385
378
                lf.end_log()
386
 
        finally:
387
 
            self.branch.unlock()
388
379
 
389
380
    def _show_body(self, lf):
390
381
        """Show the main log output.
414
405
                lf.log_revision(lr)
415
406
        except errors.GhostRevisionUnusableHere:
416
407
            raise errors.BzrCommandError(
417
 
                    gettext('Further revision history missing.'))
 
408
                gettext('Further revision history missing.'))
418
409
        lf.show_advice()
419
410
 
420
411
    def _generator_factory(self, branch, rqst):
456
447
        for revs in revision_iterator:
457
448
            for (rev_id, revno, merge_depth), rev, delta in revs:
458
449
                # 0 levels means show everything; merge_depth counts from 0
459
 
                if levels != 0 and merge_depth >= levels:
 
450
                if (levels != 0 and merge_depth is not None and
 
451
                        merge_depth >= levels):
460
452
                    continue
461
453
                if omit_merges and len(rev.parent_ids) > 1:
462
454
                    continue
470
462
                    signature = format_signature_validity(rev_id, self.branch)
471
463
                else:
472
464
                    signature = None
473
 
                yield LogRevision(rev, revno, merge_depth, delta,
 
465
                yield LogRevision(
 
466
                    rev, revno, merge_depth, delta,
474
467
                    self.rev_tag_dict.get(rev_id), diff, signature)
475
468
                if limit:
476
469
                    log_count += 1
493
486
        s = BytesIO()
494
487
        path_encoding = get_diff_header_encoding()
495
488
        diff.show_diff_trees(tree_1, tree_2, s, specific_files, old_label='',
496
 
            new_label='', path_encoding=path_encoding)
 
489
                             new_label='', path_encoding=path_encoding)
497
490
        return s.getvalue()
498
491
 
499
492
    def _create_log_revision_iterator(self):
513
506
            # not a directory
514
507
            file_count = len(self.rqst.get('specific_fileids'))
515
508
            if file_count != 1:
516
 
                raise BzrError("illegal LogRequest: must match-using-deltas "
 
509
                raise errors.BzrError(
 
510
                    "illegal LogRequest: must match-using-deltas "
517
511
                    "when logging %d files" % file_count)
518
512
            return self._log_revision_iterator_using_per_file_graph()
519
513
 
522
516
        rqst = self.rqst
523
517
        generate_merge_revisions = rqst.get('levels') != 1
524
518
        delayed_graph_generation = not rqst.get('specific_fileids') and (
525
 
                rqst.get('limit') or self.start_rev_id or self.end_rev_id)
 
519
            rqst.get('limit') or self.start_rev_id or self.end_rev_id)
526
520
        view_revisions = _calc_view_revisions(
527
521
            self.branch, self.start_rev_id, self.end_rev_id,
528
522
            rqst.get('direction'),
532
526
 
533
527
        # Apply the other filters
534
528
        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'))
 
529
                                     rqst.get('delta_type'), rqst.get('match'),
 
530
                                     file_ids=rqst.get('specific_fileids'),
 
531
                                     direction=rqst.get('direction'))
538
532
 
539
533
    def _log_revision_iterator_using_per_file_graph(self):
540
534
        # Get the base revisions, filtering by the revision range.
548
542
        if not isinstance(view_revisions, list):
549
543
            view_revisions = list(view_revisions)
550
544
        view_revisions = _filter_revisions_touching_file_id(self.branch,
551
 
            rqst.get('specific_fileids')[0], view_revisions,
552
 
            include_merges=rqst.get('levels') != 1)
 
545
                                                            rqst.get('specific_fileids')[
 
546
                                                                0], view_revisions,
 
547
                                                            include_merges=rqst.get('levels') != 1)
553
548
        return make_log_rev_iterator(self.branch, view_revisions,
554
 
            rqst.get('delta_type'), rqst.get('match'))
 
549
                                     rqst.get('delta_type'), rqst.get('match'))
555
550
 
556
551
 
557
552
def _calc_view_revisions(branch, start_rev_id, end_rev_id, direction,
569
564
            '--exclude-common-ancestry requires two different revisions'))
570
565
    if direction not in ('reverse', 'forward'):
571
566
        raise ValueError(gettext('invalid direction %r') % direction)
572
 
    br_revno, br_rev_id = branch.last_revision_info()
573
 
    if br_revno == 0:
 
567
    br_rev_id = branch.last_revision()
 
568
    if br_rev_id == _mod_revision.NULL_REVISION:
574
569
        return []
575
570
 
576
571
    if (end_rev_id and start_rev_id == end_rev_id
577
572
        and (not generate_merge_revisions
578
573
             or not _has_merges(branch, end_rev_id))):
579
574
        # If a single revision is requested, check we can handle it
580
 
        return  _generate_one_revision(branch, end_rev_id, br_rev_id,
581
 
                                       br_revno)
 
575
        return _generate_one_revision(branch, end_rev_id, br_rev_id,
 
576
                                      branch.revno())
582
577
    if not generate_merge_revisions:
583
578
        try:
584
579
            # If we only want to see linear revisions, we can iterate ...
589
584
            # ancestor of the end limit, check it before outputting anything
590
585
            if (direction == 'forward'
591
586
                or (start_rev_id and not _is_obvious_ancestor(
592
 
                        branch, start_rev_id, end_rev_id))):
593
 
                    iter_revs = list(iter_revs)
 
587
                    branch, start_rev_id, end_rev_id))):
 
588
                iter_revs = list(iter_revs)
594
589
            if direction == 'forward':
595
590
                iter_revs = reversed(iter_revs)
596
591
            return iter_revs
628
623
    initial_revisions = []
629
624
    if delayed_graph_generation:
630
625
        try:
631
 
            for rev_id, revno, depth in  _linear_view_revisions(
632
 
                branch, start_rev_id, end_rev_id, exclude_common_ancestry):
 
626
            for rev_id, revno, depth in _linear_view_revisions(
 
627
                    branch, start_rev_id, end_rev_id, exclude_common_ancestry):
633
628
                if _has_merges(branch, rev_id):
634
629
                    # The end_rev_id can be nested down somewhere. We need an
635
630
                    # explicit ancestry check. There is an ambiguity here as we
642
637
                    # -- vila 20100319
643
638
                    graph = branch.repository.get_graph()
644
639
                    if (start_rev_id is not None
645
 
                        and not graph.is_ancestor(start_rev_id, end_rev_id)):
 
640
                            and not graph.is_ancestor(start_rev_id, end_rev_id)):
646
641
                        raise _StartNotLinearAncestor()
647
642
                    # Since we collected the revisions so far, we need to
648
643
                    # adjust end_rev_id.
657
652
            # A merge was never detected so the lower revision limit can't
658
653
            # be nested down somewhere
659
654
            raise errors.BzrCommandError(gettext('Start revision not found in'
660
 
                ' history of end revision.'))
 
655
                                                 ' history of end revision.'))
661
656
 
662
657
    # We exit the loop above because we encounter a revision with merges, from
663
658
    # this revision, we need to switch to _graph_view_revisions.
668
663
    # make forward the exact opposite display, but showing the merge revisions
669
664
    # indented at the end seems slightly nicer in that case.
670
665
    view_revisions = itertools.chain(iter(initial_revisions),
671
 
        _graph_view_revisions(branch, start_rev_id, end_rev_id,
672
 
                              rebase_initial_depths=(direction == 'reverse'),
673
 
                              exclude_common_ancestry=exclude_common_ancestry))
 
666
                                     _graph_view_revisions(branch, start_rev_id, end_rev_id,
 
667
                                                           rebase_initial_depths=(
 
668
                                                               direction == 'reverse'),
 
669
                                                           exclude_common_ancestry=exclude_common_ancestry))
674
670
    return view_revisions
675
671
 
676
672
 
708
704
            # both on mainline
709
705
            return start_dotted[0] <= end_dotted[0]
710
706
        elif (len(start_dotted) == 3 and len(end_dotted) == 3 and
711
 
            start_dotted[0:1] == end_dotted[0:1]):
 
707
              start_dotted[0:1] == end_dotted[0:1]):
712
708
            # both on same development line
713
709
            return start_dotted[2] <= end_dotted[2]
714
710
        else:
732
728
    :raises _StartNotLinearAncestor: if a start_rev_id is specified but
733
729
        is not found walking the left-hand history
734
730
    """
735
 
    br_revno, br_rev_id = branch.last_revision_info()
736
731
    repo = branch.repository
737
732
    graph = repo.get_graph()
738
733
    if start_rev_id is None and end_rev_id is None:
739
 
        cur_revno = br_revno
 
734
        if branch._format.stores_revno() or \
 
735
                config.GlobalStack().get('calculate_revnos'):
 
736
            try:
 
737
                br_revno, br_rev_id = branch.last_revision_info()
 
738
            except errors.GhostRevisionsHaveNoRevno:
 
739
                br_rev_id = branch.last_revision()
 
740
                cur_revno = None
 
741
            else:
 
742
                cur_revno = br_revno
 
743
        else:
 
744
            br_rev_id = branch.last_revision()
 
745
            cur_revno = None
 
746
 
740
747
        graph_iter = graph.iter_lefthand_ancestry(br_rev_id,
741
 
            (_mod_revision.NULL_REVISION,))
 
748
                                                  (_mod_revision.NULL_REVISION,))
742
749
        while True:
743
750
            try:
744
751
                revision_id = next(graph_iter)
746
753
                # Oops, a ghost.
747
754
                yield e.revision_id, None, None
748
755
                break
 
756
            except StopIteration:
 
757
                break
749
758
            else:
750
 
                yield revision_id, str(cur_revno), 0
751
 
                cur_revno -= 1
 
759
                yield revision_id, str(cur_revno) if cur_revno is not None else None, 0
 
760
                if cur_revno is not None:
 
761
                    cur_revno -= 1
752
762
    else:
 
763
        br_rev_id = branch.last_revision()
753
764
        if end_rev_id is None:
754
765
            end_rev_id = br_rev_id
755
766
        found_start = start_rev_id is None
756
767
        graph_iter = graph.iter_lefthand_ancestry(end_rev_id,
757
 
            (_mod_revision.NULL_REVISION,))
 
768
                                                  (_mod_revision.NULL_REVISION,))
758
769
        while True:
759
770
            try:
760
771
                revision_id = next(graph_iter)
827
838
    if view_revisions and view_revisions[0][2] and view_revisions[-1][2]:
828
839
        min_depth = min([d for r, n, d in view_revisions])
829
840
        if min_depth != 0:
830
 
            view_revisions = [(r, n, d-min_depth) for r, n, d in view_revisions]
 
841
            view_revisions = [(r, n, d - min_depth)
 
842
                              for r, n, d in view_revisions]
831
843
    return view_revisions
832
844
 
833
845
 
834
846
def make_log_rev_iterator(branch, view_revisions, generate_delta, search,
835
 
        file_ids=None, direction='reverse'):
 
847
                          file_ids=None, direction='reverse'):
836
848
    """Create a revision iterator for log.
837
849
 
838
850
    :param branch: The branch being logged.
862
874
        # It would be nicer if log adapters were first class objects
863
875
        # with custom parameters. This will do for now. IGC 20090127
864
876
        if adapter == _make_delta_filter:
865
 
            log_rev_iterator = adapter(branch, generate_delta,
866
 
                search, log_rev_iterator, file_ids, direction)
 
877
            log_rev_iterator = adapter(
 
878
                branch, generate_delta, search, log_rev_iterator, file_ids,
 
879
                direction)
867
880
        else:
868
 
            log_rev_iterator = adapter(branch, generate_delta,
869
 
                search, log_rev_iterator)
 
881
            log_rev_iterator = adapter(
 
882
                branch, generate_delta, search, log_rev_iterator)
870
883
    return log_rev_iterator
871
884
 
872
885
 
886
899
    """
887
900
    if not match:
888
901
        return log_rev_iterator
889
 
    searchRE = [(k, [re.compile(x, re.IGNORECASE) for x in v])
 
902
    # Use lazy_compile so mapping to InvalidPattern error occurs.
 
903
    searchRE = [(k, [lazy_regex.lazy_compile(x, re.IGNORECASE) for x in v])
890
904
                for k, v in match.items()]
891
905
    return _filter_re(searchRE, log_rev_iterator)
892
906
 
897
911
        if new_revs:
898
912
            yield new_revs
899
913
 
 
914
 
900
915
def _match_filter(searchRE, rev):
901
916
    strings = {
902
 
               'message': (rev.message,),
903
 
               'committer': (rev.committer,),
904
 
               'author': (rev.get_apparent_authors()),
905
 
               'bugs': list(rev.iter_bugs())
906
 
               }
 
917
        'message': (rev.message,),
 
918
        'committer': (rev.committer,),
 
919
        'author': (rev.get_apparent_authors()),
 
920
        'bugs': list(rev.iter_bugs())
 
921
        }
907
922
    strings[''] = [item for inner_list in strings.values()
908
923
                   for item in inner_list]
909
 
    for (k, v) in searchRE:
 
924
    for k, v in searchRE:
910
925
        if k in strings and not _match_any_filter(strings[k], v):
911
926
            return False
912
927
    return True
913
928
 
 
929
 
914
930
def _match_any_filter(strings, res):
915
 
    return any(re.search(s) for re in res for s in strings)
 
931
    return any(r.search(s) for r in res for s in strings)
 
932
 
916
933
 
917
934
def _make_delta_filter(branch, generate_delta, search, log_rev_iterator,
918
 
    fileids=None, direction='reverse'):
 
935
                       fileids=None, direction='reverse'):
919
936
    """Add revision deltas to a log iterator if needed.
920
937
 
921
938
    :param branch: The branch being logged.
933
950
    if not generate_delta and not fileids:
934
951
        return log_rev_iterator
935
952
    return _generate_deltas(branch.repository, log_rev_iterator,
936
 
        generate_delta, fileids, direction)
 
953
                            generate_delta, fileids, direction)
937
954
 
938
955
 
939
956
def _generate_deltas(repository, log_rev_iterator, delta_type, fileids,
940
 
    direction):
 
957
                     direction):
941
958
    """Create deltas for each batch of revisions in log_rev_iterator.
942
959
 
943
960
    If we're only generating deltas for the sake of filtering against
997
1014
      fileids set once their add or remove entry is detected respectively
998
1015
    """
999
1016
    if stop_on == 'add':
1000
 
        for item in delta.added:
1001
 
            if item[1] in fileids:
1002
 
                fileids.remove(item[1])
 
1017
        for item in delta.added + delta.copied:
 
1018
            if item.file_id in fileids:
 
1019
                fileids.remove(item.file_id)
1003
1020
    elif stop_on == 'delete':
1004
1021
        for item in delta.removed:
1005
 
            if item[1] in fileids:
1006
 
                fileids.remove(item[1])
 
1022
            if item.file_id in fileids:
 
1023
                fileids.remove(item.file_id)
1007
1024
 
1008
1025
 
1009
1026
def _make_revision_objects(branch, generate_delta, search, log_rev_iterator):
1053
1070
    :param  branch: The branch containing the revisions.
1054
1071
 
1055
1072
    :param  start_revision: The first revision to be logged.
1056
 
            For backwards compatibility this may be a mainline integer revno,
1057
1073
            but for merge revision support a RevisionInfo is expected.
1058
1074
 
1059
1075
    :param  end_revision: The last revision to be logged.
1062
1078
 
1063
1079
    :return: (start_rev_id, end_rev_id) tuple.
1064
1080
    """
1065
 
    branch_revno, branch_rev_id = branch.last_revision_info()
1066
1081
    start_rev_id = None
1067
 
    if start_revision is None:
 
1082
    start_revno = None
 
1083
    if start_revision is not None:
 
1084
        if not isinstance(start_revision, revisionspec.RevisionInfo):
 
1085
            raise TypeError(start_revision)
 
1086
        start_rev_id = start_revision.rev_id
 
1087
        start_revno = start_revision.revno
 
1088
    if start_revno is None:
1068
1089
        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)
1077
1090
 
1078
1091
    end_rev_id = None
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)
 
1092
    end_revno = None
 
1093
    if end_revision is not None:
 
1094
        if not isinstance(end_revision, revisionspec.RevisionInfo):
 
1095
            raise TypeError(start_revision)
 
1096
        end_rev_id = end_revision.rev_id
 
1097
        end_revno = end_revision.revno
1089
1098
 
1090
 
    if branch_revno != 0:
 
1099
    if branch.last_revision() != _mod_revision.NULL_REVISION:
1091
1100
        if (start_rev_id == _mod_revision.NULL_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."))
 
1101
                or end_rev_id == _mod_revision.NULL_REVISION):
 
1102
            raise errors.BzrCommandError(
 
1103
                gettext('Logging revision 0 is invalid.'))
 
1104
        if end_revno is not None and start_revno > end_revno:
 
1105
            raise errors.BzrCommandError(
 
1106
                gettext("Start revision must be older than the end revision."))
1097
1107
    return (start_rev_id, end_rev_id)
1098
1108
 
1099
1109
 
1147
1157
            end_revno = end_revision
1148
1158
 
1149
1159
    if ((start_rev_id == _mod_revision.NULL_REVISION)
1150
 
        or (end_rev_id == _mod_revision.NULL_REVISION)):
 
1160
            or (end_rev_id == _mod_revision.NULL_REVISION)):
1151
1161
        raise errors.BzrCommandError(gettext('Logging revision 0 is invalid.'))
1152
1162
    if start_revno > end_revno:
1153
1163
        raise errors.BzrCommandError(gettext("Start revision must be older "
1154
 
                                     "than the end revision."))
 
1164
                                             "than the end revision."))
1155
1165
 
1156
1166
    if end_revno < start_revno:
1157
1167
        return None, None, None, None
1181
1191
 
1182
1192
 
1183
1193
def _filter_revisions_touching_file_id(branch, file_id, view_revisions,
1184
 
    include_merges=True):
 
1194
                                       include_merges=True):
1185
1195
    r"""Return the list of revision ids which touch a given file id.
1186
1196
 
1187
1197
    The function filters view_revisions and returns a subset.
1268
1278
    """Reverse revisions by depth.
1269
1279
 
1270
1280
    Revisions with a different depth are sorted as a group with the previous
1271
 
    revision of that depth.  There may be no topological justification for this,
 
1281
    revision of that depth.  There may be no topological justification for this
1272
1282
    but it looks much nicer.
1273
1283
    """
1274
1284
    # Add a fake revision at start so that we can always attach sub revisions
1381
1391
        """
1382
1392
        self.to_file = to_file
1383
1393
        # 'exact' stream used to show diff, it should print content 'as is'
1384
 
        # and should not try to decode/encode it to unicode to avoid bug #328007
 
1394
        # and should not try to decode/encode it to unicode to avoid bug
 
1395
        # #328007
1385
1396
        if to_exact_file is not None:
1386
1397
            self.to_exact_file = to_exact_file
1387
1398
        else:
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
 
1399
            # XXX: somewhat hacky; this assumes it's a codec writer; it's
 
1400
            # better for code that expects to get diffs to pass in the exact
 
1401
            # file stream
1391
1402
            self.to_exact_file = getattr(to_file, 'stream', to_file)
1392
1403
        self.show_ids = show_ids
1393
1404
        self.show_timezone = show_timezone
1394
1405
        if delta_format is None:
1395
1406
            # Ensures backward compatibility
1396
 
            delta_format = 2 # long format
 
1407
            delta_format = 2  # long format
1397
1408
        self.delta_format = delta_format
1398
1409
        self.levels = levels
1399
1410
        self._show_advice = show_advice
1497
1508
        """
1498
1509
        lines = self._foreign_info_properties(revision)
1499
1510
        for key, handler in properties_handler_registry.iteritems():
1500
 
            lines.extend(self._format_properties(handler(revision)))
 
1511
            try:
 
1512
                lines.extend(self._format_properties(handler(revision)))
 
1513
            except Exception:
 
1514
                trace.log_exception_quietly()
 
1515
                trace.print_exception(sys.exc_info(), self.to_file)
1501
1516
        return lines
1502
1517
 
1503
1518
    def _foreign_info_properties(self, rev):
1511
1526
                rev.mapping.vcs.show_foreign_revid(rev.foreign_revid))
1512
1527
 
1513
1528
        # Imported foreign revision revision ids always contain :
1514
 
        if not ":" in rev.revision_id:
 
1529
        if b":" not in rev.revision_id:
1515
1530
            return []
1516
1531
 
1517
1532
        # Revision was once imported from a foreign repository
1531
1546
        return lines
1532
1547
 
1533
1548
    def show_diff(self, to_file, diff, indent):
1534
 
        for l in diff.rstrip().split('\n'):
1535
 
            to_file.write(indent + '%s\n' % (l,))
 
1549
        encoding = get_terminal_encoding()
 
1550
        for l in diff.rstrip().split(b'\n'):
 
1551
            to_file.write(indent + l.decode(encoding, 'ignore') + '\n')
1536
1552
 
1537
1553
 
1538
1554
# Separator between revisions in long format
1561
1577
 
1562
1578
    def _date_string_original_timezone(self, rev):
1563
1579
        return format_date_with_offset_in_original_timezone(rev.timestamp,
1564
 
            rev.timezone or 0)
 
1580
                                                            rev.timezone or 0)
1565
1581
 
1566
1582
    def log_revision(self, revision):
1567
1583
        """Log a revision, either merged or not."""
1569
1585
        lines = [_LONG_SEP]
1570
1586
        if revision.revno is not None:
1571
1587
            lines.append('revno: %s%s' % (revision.revno,
1572
 
                self.merge_marker(revision)))
 
1588
                                          self.merge_marker(revision)))
1573
1589
        if revision.tags:
1574
 
            lines.append('tags: %s' % (', '.join(revision.tags)))
 
1590
            lines.append('tags: %s' % (', '.join(sorted(revision.tags))))
1575
1591
        if self.show_ids or revision.revno is None:
1576
 
            lines.append('revision-id: %s' % (revision.rev.revision_id,))
 
1592
            lines.append('revision-id: %s' %
 
1593
                         (revision.rev.revision_id.decode('utf-8'),))
1577
1594
        if self.show_ids:
1578
1595
            for parent_id in revision.rev.parent_ids:
1579
 
                lines.append('parent: %s' % (parent_id,))
 
1596
                lines.append('parent: %s' % (parent_id.decode('utf-8'),))
1580
1597
        lines.extend(self.custom_properties(revision.rev))
1581
1598
 
1582
1599
        committer = revision.rev.committer
1658
1675
        to_file = self.to_file
1659
1676
        tags = ''
1660
1677
        if revision.tags:
1661
 
            tags = ' {%s}' % (', '.join(revision.tags))
 
1678
            tags = ' {%s}' % (', '.join(sorted(revision.tags)))
1662
1679
        to_file.write(indent + "%*s %s\t%s%s%s\n" % (revno_width,
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)
 
1680
                                                     revision.revno or "", self.short_author(
 
1681
                                                         revision.rev),
 
1682
                                                     format_date(revision.rev.timestamp,
 
1683
                                                                 revision.rev.timezone or 0,
 
1684
                                                                 self.show_timezone, date_fmt="%Y-%m-%d",
 
1685
                                                                 show_offset=False),
 
1686
                                                     tags, self.merge_marker(revision)))
 
1687
        self.show_properties(revision.rev, indent + offset)
1670
1688
        if self.show_ids or revision.revno is None:
1671
1689
            to_file.write(indent + offset + 'revision-id:%s\n'
1672
 
                          % (revision.rev.revision_id,))
 
1690
                          % (revision.rev.revision_id.decode('utf-8'),))
1673
1691
        if not revision.rev.message:
1674
1692
            to_file.write(indent + offset + '(no message)\n')
1675
1693
        else:
1681
1699
            # Use the standard status output to display changes
1682
1700
            from breezy.delta import report_delta
1683
1701
            report_delta(to_file, revision.delta,
1684
 
                         short_status=self.delta_format==1,
 
1702
                         short_status=self.delta_format == 1,
1685
1703
                         show_ids=self.show_ids, indent=indent + offset)
1686
1704
        if revision.diff is not None:
1687
1705
            self.show_diff(self.to_exact_file, revision.diff, '      ')
1705
1723
    def truncate(self, str, max_len):
1706
1724
        if max_len is None or len(str) <= max_len:
1707
1725
            return str
1708
 
        return str[:max_len-3] + '...'
 
1726
        return str[:max_len - 3] + '...'
1709
1727
 
1710
1728
    def date_string(self, rev):
1711
1729
        return format_date(rev.timestamp, rev.timezone or 0,
1721
1739
    def log_revision(self, revision):
1722
1740
        indent = '  ' * revision.merge_depth
1723
1741
        self.to_file.write(self.log_string(revision.revno, revision.rev,
1724
 
            self._max_chars, revision.tags, indent))
 
1742
                                           self._max_chars, revision.tags, indent))
1725
1743
        self.to_file.write('\n')
1726
1744
 
1727
1745
    def log_string(self, revno, rev, max_chars, tags=None, prefix=''):
1740
1758
            # show revno only when is not None
1741
1759
            out.append("%s:" % revno)
1742
1760
        if max_chars is not None:
1743
 
            out.append(self.truncate(self.short_author(rev), (max_chars+3)/4))
 
1761
            out.append(self.truncate(
 
1762
                self.short_author(rev), (max_chars + 3) // 4))
1744
1763
        else:
1745
1764
            out.append(self.short_author(rev))
1746
1765
        out.append(self.date_string(rev))
1747
1766
        if len(rev.parent_ids) > 1:
1748
1767
            out.append('[merge]')
1749
1768
        if tags:
1750
 
            tag_str = '{%s}' % (', '.join(tags))
 
1769
            tag_str = '{%s}' % (', '.join(sorted(tags)))
1751
1770
            out.append(tag_str)
1752
1771
        out.append(rev.get_summary())
1753
1772
        return self.truncate(prefix + " ".join(out).rstrip('\n'), max_chars)
1773
1792
 
1774
1793
        if revision.delta is not None and revision.delta.has_changed():
1775
1794
            for c in revision.delta.added + revision.delta.removed + revision.delta.modified:
1776
 
                path, = c[:1]
 
1795
                if c.path[0] is None:
 
1796
                    path = c.path[1]
 
1797
                else:
 
1798
                    path = c.path[0]
1777
1799
                to_file.write('\t* %s:\n' % (path,))
1778
 
            for c in revision.delta.renamed:
1779
 
                oldpath, newpath = c[:2]
 
1800
            for c in revision.delta.renamed + revision.delta.copied:
1780
1801
                # For renamed files, show both the old and the new path
1781
 
                to_file.write('\t* %s:\n\t* %s:\n' % (oldpath, newpath))
 
1802
                to_file.write('\t* %s:\n\t* %s:\n' % (c.path[0], c.path[1]))
1782
1803
            to_file.write('\n')
1783
1804
 
1784
1805
        if not revision.rev.message:
1837
1858
    try:
1838
1859
        return log_formatter_registry.make_formatter(name, *args, **kwargs)
1839
1860
    except KeyError:
1840
 
        raise errors.BzrCommandError(gettext("unknown log formatter: %r") % name)
 
1861
        raise errors.BzrCommandError(
 
1862
            gettext("unknown log formatter: %r") % name)
1841
1863
 
1842
1864
 
1843
1865
def author_list_all(rev):
1879
1901
    """
1880
1902
    if to_file is None:
1881
1903
        to_file = codecs.getwriter(get_terminal_encoding())(sys.stdout,
1882
 
            errors='replace')
 
1904
                                                            errors='replace')
1883
1905
    lf = log_formatter(log_format,
1884
1906
                       show_ids=False,
1885
1907
                       to_file=to_file,
1891
1913
    for i in range(max(len(new_rh), len(old_rh))):
1892
1914
        if (len(new_rh) <= i
1893
1915
            or len(old_rh) <= i
1894
 
            or new_rh[i] != old_rh[i]):
 
1916
                or new_rh[i] != old_rh[i]):
1895
1917
            base_idx = i
1896
1918
            break
1897
1919
 
1898
1920
    if base_idx is None:
1899
1921
        to_file.write('Nothing seems to have changed\n')
1900
1922
        return
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
 
1923
    # TODO: It might be nice to do something like show_log
 
1924
    # and show the merged entries. But since this is the
 
1925
    # removed revisions, it shouldn't be as important
1904
1926
    if base_idx < len(old_rh):
1905
 
        to_file.write('*'*60)
 
1927
        to_file.write('*' * 60)
1906
1928
        to_file.write('\nRemoved Revisions:\n')
1907
1929
        for i in range(base_idx, len(old_rh)):
1908
1930
            rev = branch.repository.get_revision(old_rh[i])
1909
 
            lr = LogRevision(rev, i+1, 0, None)
 
1931
            lr = LogRevision(rev, i + 1, 0, None)
1910
1932
            lf.log_revision(lr)
1911
 
        to_file.write('*'*60)
 
1933
        to_file.write('*' * 60)
1912
1934
        to_file.write('\n\n')
1913
1935
    if base_idx < len(new_rh):
1914
1936
        to_file.write('Added Revisions:\n')
1915
1937
        show_log(branch,
1916
1938
                 lf,
1917
 
                 None,
1918
1939
                 verbose=False,
1919
1940
                 direction='forward',
1920
 
                 start_revision=base_idx+1,
 
1941
                 start_revision=base_idx + 1,
1921
1942
                 end_revision=len(new_rh),
1922
1943
                 search=None)
1923
1944
 
1991
2012
    log_format = log_formatter_registry.get_default(branch)
1992
2013
    lf = log_format(show_ids=False, to_file=output, show_timezone='original')
1993
2014
    if old_history != []:
1994
 
        output.write('*'*60)
 
2015
        output.write('*' * 60)
1995
2016
        output.write('\nRemoved Revisions:\n')
1996
2017
        show_flat_log(branch.repository, old_history, old_revno, lf)
1997
 
        output.write('*'*60)
 
2018
        output.write('*' * 60)
1998
2019
        output.write('\n\n')
1999
2020
    if new_history != []:
2000
2021
        output.write('Added Revisions:\n')
2001
2022
        start_revno = new_revno - len(new_history) + 1
2002
 
        show_log(branch, lf, None, verbose=False, direction='forward',
2003
 
                 start_revision=start_revno,)
 
2023
        show_log(branch, lf, verbose=False, direction='forward',
 
2024
                 start_revision=start_revno)
2004
2025
 
2005
2026
 
2006
2027
def show_flat_log(repository, history, last_revno, lf):
2011
2032
    :param last_revno: The revno of the last revision_id in the history.
2012
2033
    :param lf: The log formatter to use.
2013
2034
    """
2014
 
    start_revno = last_revno - len(history) + 1
2015
2035
    revisions = repository.get_revisions(history)
2016
2036
    for i, rev in enumerate(revisions):
2017
2037
        lr = LogRevision(rev, i + last_revno, 0, None)
2018
2038
        lf.log_revision(lr)
2019
2039
 
2020
2040
 
2021
 
def _get_info_for_log_files(revisionspec_list, file_list, add_cleanup):
 
2041
def _get_info_for_log_files(revisionspec_list, file_list, exit_stack):
2022
2042
    """Find file-ids and kinds given a list of files and a revision range.
2023
2043
 
2024
2044
    We search for files at the end of the range. If not found there,
2028
2048
    :param file_list: the list of paths given on the command line;
2029
2049
      the first of these can be a branch location or a file path,
2030
2050
      the remainder must be file paths
2031
 
    :param add_cleanup: When the branch returned is read locked,
2032
 
      an unlock call will be queued to the cleanup.
 
2051
    :param exit_stack: When the branch returned is read locked,
 
2052
      an unlock call will be queued to the exit stack.
2033
2053
    :return: (branch, info_list, start_rev_info, end_rev_info) where
2034
2054
      info_list is a list of (relative_path, file_id, kind) tuples where
2035
2055
      kind is one of values 'directory', 'file', 'symlink', 'tree-reference'.
2038
2058
    from breezy.builtins import _get_revision_range
2039
2059
    tree, b, path = controldir.ControlDir.open_containing_tree_or_branch(
2040
2060
        file_list[0])
2041
 
    add_cleanup(b.lock_read().unlock)
 
2061
    exit_stack.enter_context(b.lock_read())
2042
2062
    # XXX: It's damn messy converting a list of paths to relative paths when
2043
2063
    # those paths might be deleted ones, they might be on a case-insensitive
2044
2064
    # filesystem and/or they might be in silly locations (like another branch).
2053
2073
        relpaths = [path] + file_list[1:]
2054
2074
    info_list = []
2055
2075
    start_rev_info, end_rev_info = _get_revision_range(revisionspec_list, b,
2056
 
        "log")
 
2076
                                                       "log")
2057
2077
    if relpaths in ([], [u'']):
2058
2078
        return b, [], start_rev_info, end_rev_info
2059
2079
    if start_rev_info is None and end_rev_info is None:
2116
2136
def _get_kind_for_file_id(tree, path, file_id):
2117
2137
    """Return the kind of a file-id or None if it doesn't exist."""
2118
2138
    if file_id is not None:
2119
 
        return tree.kind(path, file_id)
 
2139
        return tree.kind(path)
2120
2140
    else:
2121
2141
        return None
2122
2142
 
2124
2144
properties_handler_registry = registry.Registry()
2125
2145
 
2126
2146
# Use the properties handlers to print out bug information if available
 
2147
 
 
2148
 
2127
2149
def _bugs_properties_handler(revision):
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']
 
2150
    fixed_bug_urls = []
 
2151
    related_bug_urls = []
 
2152
    for bug_url, status in revision.iter_bugs():
 
2153
        if status == 'fixed':
 
2154
            fixed_bug_urls.append(bug_url)
 
2155
        elif status == 'related':
 
2156
            related_bug_urls.append(bug_url)
 
2157
    ret = {}
 
2158
    if fixed_bug_urls:
 
2159
        text = ngettext('fixes bug', 'fixes bugs', len(fixed_bug_urls))
 
2160
        ret[text] = ' '.join(fixed_bug_urls)
 
2161
    if related_bug_urls:
 
2162
        text = ngettext('related bug', 'related bugs',
 
2163
                        len(related_bug_urls))
 
2164
        ret[text] = ' '.join(related_bug_urls)
 
2165
    return ret
2133
2166
 
2134
 
        if fixed_bug_urls:
2135
 
            return {ngettext('fixes bug', 'fixes bugs', len(fixed_bug_urls)):\
2136
 
                    ' '.join(fixed_bug_urls)}
2137
 
    return {}
2138
2167
 
2139
2168
properties_handler_registry.register('bugs_properties_handler',
2140
2169
                                     _bugs_properties_handler)