/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

Merge test-run support.

Show diffs side-by-side

added added

removed removed

Lines of Context:
65
65
    controldir,
66
66
    diff,
67
67
    foreign,
68
 
    lazy_regex,
 
68
    repository as _mod_repository,
69
69
    revision as _mod_revision,
 
70
    tsort,
70
71
    )
71
72
from breezy.i18n import gettext, ngettext
72
73
""")
73
74
 
74
75
from . import (
75
76
    errors,
 
77
    lazy_regex,
76
78
    registry,
77
79
    revisionspec,
78
 
    trace,
79
80
    )
80
81
from .osutils import (
81
82
    format_date,
89
90
    range,
90
91
    zip,
91
92
    )
92
 
from .tree import InterTree
 
93
from .tree import find_previous_path
93
94
 
94
95
 
95
96
def find_touching_revisions(repository, last_revision, last_tree, last_path):
109
110
    revno = len(history)
110
111
    for revision_id in history:
111
112
        this_tree = repository.revision_tree(revision_id)
112
 
        this_intertree = InterTree.get(this_tree, last_tree)
113
 
        this_path = this_intertree.find_source_path(last_path)
 
113
        this_path = find_previous_path(last_tree, this_tree, last_path)
114
114
 
115
115
        # now we know how it was last time, and how it is in this revision.
116
116
        # are those two states effectively the same or not?
137
137
 
138
138
def show_log(branch,
139
139
             lf,
 
140
             specific_fileid=None,
140
141
             verbose=False,
141
142
             direction='reverse',
142
143
             start_revision=None,
154
155
 
155
156
    :param lf: The LogFormatter object showing the output.
156
157
 
 
158
    :param specific_fileid: If not None, list only the commits affecting the
 
159
        specified file, rather than all commits.
 
160
 
157
161
    :param verbose: If True show added/changed/deleted/renamed files.
158
162
 
159
163
    :param direction: 'reverse' (default) is latest to earliest; 'forward' is
174
178
    :param match: Dictionary of search lists to use when matching revision
175
179
      properties.
176
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
177
186
    if verbose:
178
 
        delta_type = 'full'
 
187
        if file_ids:
 
188
            delta_type = 'partial'
 
189
        else:
 
190
            delta_type = 'full'
179
191
    else:
180
192
        delta_type = None
181
193
    if show_diff:
182
 
        diff_type = 'full'
 
194
        if file_ids:
 
195
            diff_type = 'partial'
 
196
        else:
 
197
            diff_type = 'full'
183
198
    else:
184
199
        diff_type = None
185
200
 
186
201
    if isinstance(start_revision, int):
187
202
        try:
188
203
            start_revision = revisionspec.RevisionInfo(branch, start_revision)
189
 
        except (errors.NoSuchRevision, errors.RevnoOutOfBounds):
 
204
        except errors.NoSuchRevision:
190
205
            raise errors.InvalidRevisionNumber(start_revision)
191
206
 
192
207
    if isinstance(end_revision, int):
193
208
        try:
194
209
            end_revision = revisionspec.RevisionInfo(branch, end_revision)
195
 
        except (errors.NoSuchRevision, errors.RevnoOutOfBounds):
 
210
        except errors.NoSuchRevision:
196
211
            raise errors.InvalidRevisionNumber(end_revision)
197
212
 
198
213
    if end_revision is not None and end_revision.revno == 0:
199
214
        raise errors.InvalidRevisionNumber(end_revision.revno)
200
215
 
201
216
    # Build the request and execute it
202
 
    rqst = make_log_request_dict(
203
 
        direction=direction,
 
217
    rqst = make_log_request_dict(direction=direction, specific_fileids=file_ids,
204
218
        start_revision=start_revision, end_revision=end_revision,
205
219
        limit=limit, message_search=search,
206
220
        delta_type=delta_type, diff_type=diff_type)
292
306
            else:
293
307
                match['message'] = [message_search]
294
308
        else:
295
 
            match = {'message': [message_search]}
 
309
            match={ 'message': [message_search] }
296
310
    return {
297
311
        'direction': direction,
298
312
        'specific_fileids': specific_fileids,
408
422
                lf.log_revision(lr)
409
423
        except errors.GhostRevisionUnusableHere:
410
424
            raise errors.BzrCommandError(
411
 
                gettext('Further revision history missing.'))
 
425
                    gettext('Further revision history missing.'))
412
426
        lf.show_advice()
413
427
 
414
428
    def _generator_factory(self, branch, rqst):
450
464
        for revs in revision_iterator:
451
465
            for (rev_id, revno, merge_depth), rev, delta in revs:
452
466
                # 0 levels means show everything; merge_depth counts from 0
453
 
                if (levels != 0 and merge_depth is not None and
454
 
                        merge_depth >= levels):
 
467
                if levels != 0 and merge_depth >= levels:
455
468
                    continue
456
469
                if omit_merges and len(rev.parent_ids) > 1:
457
470
                    continue
465
478
                    signature = format_signature_validity(rev_id, self.branch)
466
479
                else:
467
480
                    signature = None
468
 
                yield LogRevision(
469
 
                    rev, revno, merge_depth, delta,
 
481
                yield LogRevision(rev, revno, merge_depth, delta,
470
482
                    self.rev_tag_dict.get(rev_id), diff, signature)
471
483
                if limit:
472
484
                    log_count += 1
489
501
        s = BytesIO()
490
502
        path_encoding = get_diff_header_encoding()
491
503
        diff.show_diff_trees(tree_1, tree_2, s, specific_files, old_label='',
492
 
                             new_label='', path_encoding=path_encoding)
 
504
            new_label='', path_encoding=path_encoding)
493
505
        return s.getvalue()
494
506
 
495
507
    def _create_log_revision_iterator(self):
509
521
            # not a directory
510
522
            file_count = len(self.rqst.get('specific_fileids'))
511
523
            if file_count != 1:
512
 
                raise errors.BzrError(
513
 
                    "illegal LogRequest: must match-using-deltas "
 
524
                raise BzrError("illegal LogRequest: must match-using-deltas "
514
525
                    "when logging %d files" % file_count)
515
526
            return self._log_revision_iterator_using_per_file_graph()
516
527
 
519
530
        rqst = self.rqst
520
531
        generate_merge_revisions = rqst.get('levels') != 1
521
532
        delayed_graph_generation = not rqst.get('specific_fileids') and (
522
 
            rqst.get('limit') or self.start_rev_id or self.end_rev_id)
 
533
                rqst.get('limit') or self.start_rev_id or self.end_rev_id)
523
534
        view_revisions = _calc_view_revisions(
524
535
            self.branch, self.start_rev_id, self.end_rev_id,
525
536
            rqst.get('direction'),
529
540
 
530
541
        # Apply the other filters
531
542
        return make_log_rev_iterator(self.branch, view_revisions,
532
 
                                     rqst.get('delta_type'), rqst.get('match'),
533
 
                                     file_ids=rqst.get('specific_fileids'),
534
 
                                     direction=rqst.get('direction'))
 
543
            rqst.get('delta_type'), rqst.get('match'),
 
544
            file_ids=rqst.get('specific_fileids'),
 
545
            direction=rqst.get('direction'))
535
546
 
536
547
    def _log_revision_iterator_using_per_file_graph(self):
537
548
        # Get the base revisions, filtering by the revision range.
545
556
        if not isinstance(view_revisions, list):
546
557
            view_revisions = list(view_revisions)
547
558
        view_revisions = _filter_revisions_touching_file_id(self.branch,
548
 
                                                            rqst.get('specific_fileids')[
549
 
                                                                0], view_revisions,
550
 
                                                            include_merges=rqst.get('levels') != 1)
 
559
            rqst.get('specific_fileids')[0], view_revisions,
 
560
            include_merges=rqst.get('levels') != 1)
551
561
        return make_log_rev_iterator(self.branch, view_revisions,
552
 
                                     rqst.get('delta_type'), rqst.get('match'))
 
562
            rqst.get('delta_type'), rqst.get('match'))
553
563
 
554
564
 
555
565
def _calc_view_revisions(branch, start_rev_id, end_rev_id, direction,
575
585
        and (not generate_merge_revisions
576
586
             or not _has_merges(branch, end_rev_id))):
577
587
        # If a single revision is requested, check we can handle it
578
 
        return _generate_one_revision(branch, end_rev_id, br_rev_id,
579
 
                                      branch.revno())
 
588
        return  _generate_one_revision(branch, end_rev_id, br_rev_id,
 
589
                                       branch.revno())
580
590
    if not generate_merge_revisions:
581
591
        try:
582
592
            # If we only want to see linear revisions, we can iterate ...
587
597
            # ancestor of the end limit, check it before outputting anything
588
598
            if (direction == 'forward'
589
599
                or (start_rev_id and not _is_obvious_ancestor(
590
 
                    branch, start_rev_id, end_rev_id))):
591
 
                iter_revs = list(iter_revs)
 
600
                        branch, start_rev_id, end_rev_id))):
 
601
                    iter_revs = list(iter_revs)
592
602
            if direction == 'forward':
593
603
                iter_revs = reversed(iter_revs)
594
604
            return iter_revs
626
636
    initial_revisions = []
627
637
    if delayed_graph_generation:
628
638
        try:
629
 
            for rev_id, revno, depth in _linear_view_revisions(
630
 
                    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):
631
641
                if _has_merges(branch, rev_id):
632
642
                    # The end_rev_id can be nested down somewhere. We need an
633
643
                    # explicit ancestry check. There is an ambiguity here as we
640
650
                    # -- vila 20100319
641
651
                    graph = branch.repository.get_graph()
642
652
                    if (start_rev_id is not None
643
 
                            and not graph.is_ancestor(start_rev_id, end_rev_id)):
 
653
                        and not graph.is_ancestor(start_rev_id, end_rev_id)):
644
654
                        raise _StartNotLinearAncestor()
645
655
                    # Since we collected the revisions so far, we need to
646
656
                    # adjust end_rev_id.
655
665
            # A merge was never detected so the lower revision limit can't
656
666
            # be nested down somewhere
657
667
            raise errors.BzrCommandError(gettext('Start revision not found in'
658
 
                                                 ' history of end revision.'))
 
668
                ' history of end revision.'))
659
669
 
660
670
    # We exit the loop above because we encounter a revision with merges, from
661
671
    # this revision, we need to switch to _graph_view_revisions.
666
676
    # make forward the exact opposite display, but showing the merge revisions
667
677
    # indented at the end seems slightly nicer in that case.
668
678
    view_revisions = itertools.chain(iter(initial_revisions),
669
 
                                     _graph_view_revisions(branch, start_rev_id, end_rev_id,
670
 
                                                           rebase_initial_depths=(
671
 
                                                               direction == 'reverse'),
672
 
                                                           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))
673
682
    return view_revisions
674
683
 
675
684
 
707
716
            # both on mainline
708
717
            return start_dotted[0] <= end_dotted[0]
709
718
        elif (len(start_dotted) == 3 and len(end_dotted) == 3 and
710
 
              start_dotted[0:1] == end_dotted[0:1]):
 
719
            start_dotted[0:1] == end_dotted[0:1]):
711
720
            # both on same development line
712
721
            return start_dotted[2] <= end_dotted[2]
713
722
        else:
734
743
    repo = branch.repository
735
744
    graph = repo.get_graph()
736
745
    if start_rev_id is None and end_rev_id is None:
737
 
        if branch._format.stores_revno() or \
738
 
                config.GlobalStack().get('calculate_revnos'):
739
 
            try:
740
 
                br_revno, br_rev_id = branch.last_revision_info()
741
 
            except errors.GhostRevisionsHaveNoRevno:
742
 
                br_rev_id = branch.last_revision()
743
 
                cur_revno = None
744
 
            else:
745
 
                cur_revno = br_revno
746
 
        else:
 
746
        try:
 
747
            br_revno, br_rev_id = branch.last_revision_info()
 
748
        except errors.GhostRevisionsHaveNoRevno:
747
749
            br_rev_id = branch.last_revision()
748
750
            cur_revno = None
749
 
 
 
751
        else:
 
752
            cur_revno = br_revno
750
753
        graph_iter = graph.iter_lefthand_ancestry(br_rev_id,
751
 
                                                  (_mod_revision.NULL_REVISION,))
 
754
            (_mod_revision.NULL_REVISION,))
752
755
        while True:
753
756
            try:
754
757
                revision_id = next(graph_iter)
756
759
                # Oops, a ghost.
757
760
                yield e.revision_id, None, None
758
761
                break
759
 
            except StopIteration:
760
 
                break
761
762
            else:
762
763
                yield revision_id, str(cur_revno) if cur_revno is not None else None, 0
763
764
                if cur_revno is not None:
768
769
            end_rev_id = br_rev_id
769
770
        found_start = start_rev_id is None
770
771
        graph_iter = graph.iter_lefthand_ancestry(end_rev_id,
771
 
                                                  (_mod_revision.NULL_REVISION,))
 
772
            (_mod_revision.NULL_REVISION,))
772
773
        while True:
773
774
            try:
774
775
                revision_id = next(graph_iter)
841
842
    if view_revisions and view_revisions[0][2] and view_revisions[-1][2]:
842
843
        min_depth = min([d for r, n, d in view_revisions])
843
844
        if min_depth != 0:
844
 
            view_revisions = [(r, n, d - min_depth)
845
 
                              for r, n, d in view_revisions]
 
845
            view_revisions = [(r, n, d-min_depth) for r, n, d in view_revisions]
846
846
    return view_revisions
847
847
 
848
848
 
849
849
def make_log_rev_iterator(branch, view_revisions, generate_delta, search,
850
 
                          file_ids=None, direction='reverse'):
 
850
        file_ids=None, direction='reverse'):
851
851
    """Create a revision iterator for log.
852
852
 
853
853
    :param branch: The branch being logged.
877
877
        # It would be nicer if log adapters were first class objects
878
878
        # with custom parameters. This will do for now. IGC 20090127
879
879
        if adapter == _make_delta_filter:
880
 
            log_rev_iterator = adapter(
881
 
                branch, generate_delta, search, log_rev_iterator, file_ids,
882
 
                direction)
 
880
            log_rev_iterator = adapter(branch, generate_delta,
 
881
                search, log_rev_iterator, file_ids, direction)
883
882
        else:
884
 
            log_rev_iterator = adapter(
885
 
                branch, generate_delta, search, log_rev_iterator)
 
883
            log_rev_iterator = adapter(branch, generate_delta,
 
884
                search, log_rev_iterator)
886
885
    return log_rev_iterator
887
886
 
888
887
 
902
901
    """
903
902
    if not match:
904
903
        return log_rev_iterator
905
 
    # Use lazy_compile so mapping to InvalidPattern error occurs.
906
 
    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])
907
905
                for k, v in match.items()]
908
906
    return _filter_re(searchRE, log_rev_iterator)
909
907
 
914
912
        if new_revs:
915
913
            yield new_revs
916
914
 
917
 
 
918
915
def _match_filter(searchRE, rev):
919
916
    strings = {
920
 
        'message': (rev.message,),
921
 
        'committer': (rev.committer,),
922
 
        'author': (rev.get_apparent_authors()),
923
 
        'bugs': list(rev.iter_bugs())
924
 
        }
 
917
               'message': (rev.message,),
 
918
               'committer': (rev.committer,),
 
919
               'author': (rev.get_apparent_authors()),
 
920
               'bugs': list(rev.iter_bugs())
 
921
               }
925
922
    strings[''] = [item for inner_list in strings.values()
926
923
                   for item in inner_list]
927
 
    for k, v in searchRE:
 
924
    for (k, v) in searchRE:
928
925
        if k in strings and not _match_any_filter(strings[k], v):
929
926
            return False
930
927
    return True
931
928
 
932
 
 
933
929
def _match_any_filter(strings, res):
934
 
    return any(r.search(s) for r in res for s in strings)
935
 
 
 
930
    return any(re.search(s) for re in res for s in strings)
936
931
 
937
932
def _make_delta_filter(branch, generate_delta, search, log_rev_iterator,
938
 
                       fileids=None, direction='reverse'):
 
933
    fileids=None, direction='reverse'):
939
934
    """Add revision deltas to a log iterator if needed.
940
935
 
941
936
    :param branch: The branch being logged.
953
948
    if not generate_delta and not fileids:
954
949
        return log_rev_iterator
955
950
    return _generate_deltas(branch.repository, log_rev_iterator,
956
 
                            generate_delta, fileids, direction)
 
951
        generate_delta, fileids, direction)
957
952
 
958
953
 
959
954
def _generate_deltas(repository, log_rev_iterator, delta_type, fileids,
960
 
                     direction):
 
955
    direction):
961
956
    """Create deltas for each batch of revisions in log_rev_iterator.
962
957
 
963
958
    If we're only generating deltas for the sake of filtering against
1017
1012
      fileids set once their add or remove entry is detected respectively
1018
1013
    """
1019
1014
    if stop_on == 'add':
1020
 
        for item in delta.added + delta.copied:
1021
 
            if item.file_id in fileids:
1022
 
                fileids.remove(item.file_id)
 
1015
        for item in delta.added:
 
1016
            if item[1] in fileids:
 
1017
                fileids.remove(item[1])
1023
1018
    elif stop_on == 'delete':
1024
1019
        for item in delta.removed:
1025
 
            if item.file_id in fileids:
1026
 
                fileids.remove(item.file_id)
 
1020
            if item[1] in fileids:
 
1021
                fileids.remove(item[1])
1027
1022
 
1028
1023
 
1029
1024
def _make_revision_objects(branch, generate_delta, search, log_rev_iterator):
1098
1093
            raise TypeError(start_revision)
1099
1094
        end_rev_id = end_revision.rev_id
1100
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
1101
1101
 
1102
1102
    if branch.last_revision() != _mod_revision.NULL_REVISION:
1103
1103
        if (start_rev_id == _mod_revision.NULL_REVISION
1104
 
                or end_rev_id == _mod_revision.NULL_REVISION):
1105
 
            raise errors.BzrCommandError(
1106
 
                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.'))
1107
1106
        if end_revno is not None and start_revno > end_revno:
1108
 
            raise errors.BzrCommandError(
1109
 
                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."))
1110
1109
    return (start_rev_id, end_rev_id)
1111
1110
 
1112
1111
 
1160
1159
            end_revno = end_revision
1161
1160
 
1162
1161
    if ((start_rev_id == _mod_revision.NULL_REVISION)
1163
 
            or (end_rev_id == _mod_revision.NULL_REVISION)):
 
1162
        or (end_rev_id == _mod_revision.NULL_REVISION)):
1164
1163
        raise errors.BzrCommandError(gettext('Logging revision 0 is invalid.'))
1165
1164
    if start_revno > end_revno:
1166
1165
        raise errors.BzrCommandError(gettext("Start revision must be older "
1167
 
                                             "than the end revision."))
 
1166
                                     "than the end revision."))
1168
1167
 
1169
1168
    if end_revno < start_revno:
1170
1169
        return None, None, None, None
1194
1193
 
1195
1194
 
1196
1195
def _filter_revisions_touching_file_id(branch, file_id, view_revisions,
1197
 
                                       include_merges=True):
 
1196
    include_merges=True):
1198
1197
    r"""Return the list of revision ids which touch a given file id.
1199
1198
 
1200
1199
    The function filters view_revisions and returns a subset.
1281
1280
    """Reverse revisions by depth.
1282
1281
 
1283
1282
    Revisions with a different depth are sorted as a group with the previous
1284
 
    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,
1285
1284
    but it looks much nicer.
1286
1285
    """
1287
1286
    # Add a fake revision at start so that we can always attach sub revisions
1394
1393
        """
1395
1394
        self.to_file = to_file
1396
1395
        # 'exact' stream used to show diff, it should print content 'as is'
1397
 
        # and should not try to decode/encode it to unicode to avoid bug
1398
 
        # #328007
 
1396
        # and should not try to decode/encode it to unicode to avoid bug #328007
1399
1397
        if to_exact_file is not None:
1400
1398
            self.to_exact_file = to_exact_file
1401
1399
        else:
1402
 
            # XXX: somewhat hacky; this assumes it's a codec writer; it's
1403
 
            # better for code that expects to get diffs to pass in the exact
1404
 
            # 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
1405
1403
            self.to_exact_file = getattr(to_file, 'stream', to_file)
1406
1404
        self.show_ids = show_ids
1407
1405
        self.show_timezone = show_timezone
1408
1406
        if delta_format is None:
1409
1407
            # Ensures backward compatibility
1410
 
            delta_format = 2  # long format
 
1408
            delta_format = 2 # long format
1411
1409
        self.delta_format = delta_format
1412
1410
        self.levels = levels
1413
1411
        self._show_advice = show_advice
1511
1509
        """
1512
1510
        lines = self._foreign_info_properties(revision)
1513
1511
        for key, handler in properties_handler_registry.iteritems():
1514
 
            try:
1515
 
                lines.extend(self._format_properties(handler(revision)))
1516
 
            except Exception:
1517
 
                trace.log_exception_quietly()
1518
 
                trace.print_exception(sys.exc_info(), self.to_file)
 
1512
            lines.extend(self._format_properties(handler(revision)))
1519
1513
        return lines
1520
1514
 
1521
1515
    def _foreign_info_properties(self, rev):
1529
1523
                rev.mapping.vcs.show_foreign_revid(rev.foreign_revid))
1530
1524
 
1531
1525
        # Imported foreign revision revision ids always contain :
1532
 
        if b":" not in rev.revision_id:
 
1526
        if not b":" in rev.revision_id:
1533
1527
            return []
1534
1528
 
1535
1529
        # Revision was once imported from a foreign repository
1549
1543
        return lines
1550
1544
 
1551
1545
    def show_diff(self, to_file, diff, indent):
1552
 
        encoding = get_terminal_encoding()
1553
 
        for l in diff.rstrip().split(b'\n'):
1554
 
            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,))
1555
1548
 
1556
1549
 
1557
1550
# Separator between revisions in long format
1580
1573
 
1581
1574
    def _date_string_original_timezone(self, rev):
1582
1575
        return format_date_with_offset_in_original_timezone(rev.timestamp,
1583
 
                                                            rev.timezone or 0)
 
1576
            rev.timezone or 0)
1584
1577
 
1585
1578
    def log_revision(self, revision):
1586
1579
        """Log a revision, either merged or not."""
1588
1581
        lines = [_LONG_SEP]
1589
1582
        if revision.revno is not None:
1590
1583
            lines.append('revno: %s%s' % (revision.revno,
1591
 
                                          self.merge_marker(revision)))
 
1584
                self.merge_marker(revision)))
1592
1585
        if revision.tags:
1593
 
            lines.append('tags: %s' % (', '.join(sorted(revision.tags))))
 
1586
            lines.append('tags: %s' % (', '.join(revision.tags)))
1594
1587
        if self.show_ids or revision.revno is None:
1595
 
            lines.append('revision-id: %s' %
1596
 
                         (revision.rev.revision_id.decode('utf-8'),))
 
1588
            lines.append('revision-id: %s' % (revision.rev.revision_id,))
1597
1589
        if self.show_ids:
1598
1590
            for parent_id in revision.rev.parent_ids:
1599
 
                lines.append('parent: %s' % (parent_id.decode('utf-8'),))
 
1591
                lines.append('parent: %s' % (parent_id,))
1600
1592
        lines.extend(self.custom_properties(revision.rev))
1601
1593
 
1602
1594
        committer = revision.rev.committer
1678
1670
        to_file = self.to_file
1679
1671
        tags = ''
1680
1672
        if revision.tags:
1681
 
            tags = ' {%s}' % (', '.join(sorted(revision.tags)))
 
1673
            tags = ' {%s}' % (', '.join(revision.tags))
1682
1674
        to_file.write(indent + "%*s %s\t%s%s%s\n" % (revno_width,
1683
 
                                                     revision.revno or "", self.short_author(
1684
 
                                                         revision.rev),
1685
 
                                                     format_date(revision.rev.timestamp,
1686
 
                                                                 revision.rev.timezone or 0,
1687
 
                                                                 self.show_timezone, date_fmt="%Y-%m-%d",
1688
 
                                                                 show_offset=False),
1689
 
                                                     tags, self.merge_marker(revision)))
1690
 
        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)
1691
1682
        if self.show_ids or revision.revno is None:
1692
1683
            to_file.write(indent + offset + 'revision-id:%s\n'
1693
 
                          % (revision.rev.revision_id.decode('utf-8'),))
 
1684
                          % (revision.rev.revision_id,))
1694
1685
        if not revision.rev.message:
1695
1686
            to_file.write(indent + offset + '(no message)\n')
1696
1687
        else:
1702
1693
            # Use the standard status output to display changes
1703
1694
            from breezy.delta import report_delta
1704
1695
            report_delta(to_file, revision.delta,
1705
 
                         short_status=self.delta_format == 1,
 
1696
                         short_status=self.delta_format==1,
1706
1697
                         show_ids=self.show_ids, indent=indent + offset)
1707
1698
        if revision.diff is not None:
1708
1699
            self.show_diff(self.to_exact_file, revision.diff, '      ')
1726
1717
    def truncate(self, str, max_len):
1727
1718
        if max_len is None or len(str) <= max_len:
1728
1719
            return str
1729
 
        return str[:max_len - 3] + '...'
 
1720
        return str[:max_len-3] + '...'
1730
1721
 
1731
1722
    def date_string(self, rev):
1732
1723
        return format_date(rev.timestamp, rev.timezone or 0,
1742
1733
    def log_revision(self, revision):
1743
1734
        indent = '  ' * revision.merge_depth
1744
1735
        self.to_file.write(self.log_string(revision.revno, revision.rev,
1745
 
                                           self._max_chars, revision.tags, indent))
 
1736
            self._max_chars, revision.tags, indent))
1746
1737
        self.to_file.write('\n')
1747
1738
 
1748
1739
    def log_string(self, revno, rev, max_chars, tags=None, prefix=''):
1761
1752
            # show revno only when is not None
1762
1753
            out.append("%s:" % revno)
1763
1754
        if max_chars is not None:
1764
 
            out.append(self.truncate(
1765
 
                self.short_author(rev), (max_chars + 3) // 4))
 
1755
            out.append(self.truncate(self.short_author(rev), (max_chars+3)/4))
1766
1756
        else:
1767
1757
            out.append(self.short_author(rev))
1768
1758
        out.append(self.date_string(rev))
1769
1759
        if len(rev.parent_ids) > 1:
1770
1760
            out.append('[merge]')
1771
1761
        if tags:
1772
 
            tag_str = '{%s}' % (', '.join(sorted(tags)))
 
1762
            tag_str = '{%s}' % (', '.join(tags))
1773
1763
            out.append(tag_str)
1774
1764
        out.append(rev.get_summary())
1775
1765
        return self.truncate(prefix + " ".join(out).rstrip('\n'), max_chars)
1795
1785
 
1796
1786
        if revision.delta is not None and revision.delta.has_changed():
1797
1787
            for c in revision.delta.added + revision.delta.removed + revision.delta.modified:
1798
 
                if c.path[0] is None:
1799
 
                    path = c.path[1]
1800
 
                else:
1801
 
                    path = c.path[0]
 
1788
                path, = c[:1]
1802
1789
                to_file.write('\t* %s:\n' % (path,))
1803
 
            for c in revision.delta.renamed + revision.delta.copied:
 
1790
            for c in revision.delta.renamed:
 
1791
                oldpath, newpath = c[:2]
1804
1792
                # For renamed files, show both the old and the new path
1805
 
                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))
1806
1794
            to_file.write('\n')
1807
1795
 
1808
1796
        if not revision.rev.message:
1861
1849
    try:
1862
1850
        return log_formatter_registry.make_formatter(name, *args, **kwargs)
1863
1851
    except KeyError:
1864
 
        raise errors.BzrCommandError(
1865
 
            gettext("unknown log formatter: %r") % name)
 
1852
        raise errors.BzrCommandError(gettext("unknown log formatter: %r") % name)
1866
1853
 
1867
1854
 
1868
1855
def author_list_all(rev):
1904
1891
    """
1905
1892
    if to_file is None:
1906
1893
        to_file = codecs.getwriter(get_terminal_encoding())(sys.stdout,
1907
 
                                                            errors='replace')
 
1894
            errors='replace')
1908
1895
    lf = log_formatter(log_format,
1909
1896
                       show_ids=False,
1910
1897
                       to_file=to_file,
1916
1903
    for i in range(max(len(new_rh), len(old_rh))):
1917
1904
        if (len(new_rh) <= i
1918
1905
            or len(old_rh) <= i
1919
 
                or new_rh[i] != old_rh[i]):
 
1906
            or new_rh[i] != old_rh[i]):
1920
1907
            base_idx = i
1921
1908
            break
1922
1909
 
1923
1910
    if base_idx is None:
1924
1911
        to_file.write('Nothing seems to have changed\n')
1925
1912
        return
1926
 
    # TODO: It might be nice to do something like show_log
1927
 
    # and show the merged entries. But since this is the
1928
 
    # 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
1929
1916
    if base_idx < len(old_rh):
1930
 
        to_file.write('*' * 60)
 
1917
        to_file.write('*'*60)
1931
1918
        to_file.write('\nRemoved Revisions:\n')
1932
1919
        for i in range(base_idx, len(old_rh)):
1933
1920
            rev = branch.repository.get_revision(old_rh[i])
1934
 
            lr = LogRevision(rev, i + 1, 0, None)
 
1921
            lr = LogRevision(rev, i+1, 0, None)
1935
1922
            lf.log_revision(lr)
1936
 
        to_file.write('*' * 60)
 
1923
        to_file.write('*'*60)
1937
1924
        to_file.write('\n\n')
1938
1925
    if base_idx < len(new_rh):
1939
1926
        to_file.write('Added Revisions:\n')
1940
1927
        show_log(branch,
1941
1928
                 lf,
 
1929
                 None,
1942
1930
                 verbose=False,
1943
1931
                 direction='forward',
1944
 
                 start_revision=base_idx + 1,
 
1932
                 start_revision=base_idx+1,
1945
1933
                 end_revision=len(new_rh),
1946
1934
                 search=None)
1947
1935
 
2015
2003
    log_format = log_formatter_registry.get_default(branch)
2016
2004
    lf = log_format(show_ids=False, to_file=output, show_timezone='original')
2017
2005
    if old_history != []:
2018
 
        output.write('*' * 60)
 
2006
        output.write('*'*60)
2019
2007
        output.write('\nRemoved Revisions:\n')
2020
2008
        show_flat_log(branch.repository, old_history, old_revno, lf)
2021
 
        output.write('*' * 60)
 
2009
        output.write('*'*60)
2022
2010
        output.write('\n\n')
2023
2011
    if new_history != []:
2024
2012
        output.write('Added Revisions:\n')
2025
2013
        start_revno = new_revno - len(new_history) + 1
2026
 
        show_log(branch, lf, verbose=False, direction='forward',
 
2014
        show_log(branch, lf, None, verbose=False, direction='forward',
2027
2015
                 start_revision=start_revno)
2028
2016
 
2029
2017
 
2035
2023
    :param last_revno: The revno of the last revision_id in the history.
2036
2024
    :param lf: The log formatter to use.
2037
2025
    """
 
2026
    start_revno = last_revno - len(history) + 1
2038
2027
    revisions = repository.get_revisions(history)
2039
2028
    for i, rev in enumerate(revisions):
2040
2029
        lr = LogRevision(rev, i + last_revno, 0, None)
2041
2030
        lf.log_revision(lr)
2042
2031
 
2043
2032
 
2044
 
def _get_info_for_log_files(revisionspec_list, file_list, exit_stack):
 
2033
def _get_info_for_log_files(revisionspec_list, file_list, add_cleanup):
2045
2034
    """Find file-ids and kinds given a list of files and a revision range.
2046
2035
 
2047
2036
    We search for files at the end of the range. If not found there,
2051
2040
    :param file_list: the list of paths given on the command line;
2052
2041
      the first of these can be a branch location or a file path,
2053
2042
      the remainder must be file paths
2054
 
    :param exit_stack: When the branch returned is read locked,
2055
 
      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.
2056
2045
    :return: (branch, info_list, start_rev_info, end_rev_info) where
2057
2046
      info_list is a list of (relative_path, file_id, kind) tuples where
2058
2047
      kind is one of values 'directory', 'file', 'symlink', 'tree-reference'.
2061
2050
    from breezy.builtins import _get_revision_range
2062
2051
    tree, b, path = controldir.ControlDir.open_containing_tree_or_branch(
2063
2052
        file_list[0])
2064
 
    exit_stack.enter_context(b.lock_read())
 
2053
    add_cleanup(b.lock_read().unlock)
2065
2054
    # XXX: It's damn messy converting a list of paths to relative paths when
2066
2055
    # those paths might be deleted ones, they might be on a case-insensitive
2067
2056
    # filesystem and/or they might be in silly locations (like another branch).
2076
2065
        relpaths = [path] + file_list[1:]
2077
2066
    info_list = []
2078
2067
    start_rev_info, end_rev_info = _get_revision_range(revisionspec_list, b,
2079
 
                                                       "log")
 
2068
        "log")
2080
2069
    if relpaths in ([], [u'']):
2081
2070
        return b, [], start_rev_info, end_rev_info
2082
2071
    if start_rev_info is None and end_rev_info is None:
2139
2128
def _get_kind_for_file_id(tree, path, file_id):
2140
2129
    """Return the kind of a file-id or None if it doesn't exist."""
2141
2130
    if file_id is not None:
2142
 
        return tree.kind(path)
 
2131
        return tree.kind(path, file_id)
2143
2132
    else:
2144
2133
        return None
2145
2134
 
2147
2136
properties_handler_registry = registry.Registry()
2148
2137
 
2149
2138
# Use the properties handlers to print out bug information if available
2150
 
 
2151
 
 
2152
2139
def _bugs_properties_handler(revision):
2153
 
    fixed_bug_urls = []
2154
 
    related_bug_urls = []
2155
 
    for bug_url, status in revision.iter_bugs():
2156
 
        if status == 'fixed':
2157
 
            fixed_bug_urls.append(bug_url)
2158
 
        elif status == 'related':
2159
 
            related_bug_urls.append(bug_url)
2160
 
    ret = {}
2161
 
    if fixed_bug_urls:
2162
 
        text = ngettext('fixes bug', 'fixes bugs', len(fixed_bug_urls))
2163
 
        ret[text] = ' '.join(fixed_bug_urls)
2164
 
    if related_bug_urls:
2165
 
        text = ngettext('related bug', 'related bugs',
2166
 
                        len(related_bug_urls))
2167
 
        ret[text] = ' '.join(related_bug_urls)
2168
 
    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']
2169
2145
 
 
2146
        if fixed_bug_urls:
 
2147
            return {ngettext('fixes bug', 'fixes bugs', len(fixed_bug_urls)):\
 
2148
                    ' '.join(fixed_bug_urls)}
 
2149
    return {}
2170
2150
 
2171
2151
properties_handler_registry.register('bugs_properties_handler',
2172
2152
                                     _bugs_properties_handler)