/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 20:02:36 UTC
  • mto: (7490.7.7 work)
  • mto: This revision was merged to the branch mainline in revision 7501.
  • Revision ID: jelmer@jelmer.uk-20200322200236-fsbl91ktcn6fcbdd
Fix tests.

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
81
82
    format_date_with_offset_in_original_timezone,
82
83
    get_diff_header_encoding,
83
84
    get_terminal_encoding,
84
 
    is_inside,
85
85
    terminal_width,
86
86
    )
87
 
from .tree import (
88
 
    find_previous_path,
89
 
    InterTree,
 
87
from .sixish import (
 
88
    BytesIO,
 
89
    range,
 
90
    zip,
90
91
    )
 
92
from .tree import InterTree
91
93
 
92
94
 
93
95
def find_touching_revisions(repository, last_revision, last_tree, last_path):
94
 
    """Yield a description of revisions which affect the file.
 
96
    """Yield a description of revisions which affect the file_id.
95
97
 
96
98
    Each returned element is (revno, revision_id, description)
97
99
 
139
141
             direction='reverse',
140
142
             start_revision=None,
141
143
             end_revision=None,
 
144
             search=None,
142
145
             limit=None,
143
146
             show_diff=False,
144
147
             match=None):
160
163
 
161
164
    :param end_revision: If not None, only show revisions <= end_revision
162
165
 
 
166
    :param search: If not None, only show revisions with matching commit
 
167
        messages
 
168
 
163
169
    :param limit: If set, shows only 'limit' revisions, all revisions are shown
164
170
        if None or 0.
165
171
 
196
202
    rqst = make_log_request_dict(
197
203
        direction=direction,
198
204
        start_revision=start_revision, end_revision=end_revision,
199
 
        limit=limit, delta_type=delta_type, diff_type=diff_type)
 
205
        limit=limit, message_search=search,
 
206
        delta_type=delta_type, diff_type=diff_type)
200
207
    Logger(branch, rqst).show(lf)
201
208
 
202
209
 
211
218
    }
212
219
 
213
220
 
214
 
def make_log_request_dict(direction='reverse', specific_files=None,
 
221
def make_log_request_dict(direction='reverse', specific_fileids=None,
215
222
                          start_revision=None, end_revision=None, limit=None,
216
223
                          message_search=None, levels=None, generate_tags=True,
217
224
                          delta_type=None,
228
235
    :param direction: 'reverse' (default) is latest to earliest;
229
236
      'forward' is earliest to latest.
230
237
 
231
 
    :param specific_files: If not None, only include revisions
 
238
    :param specific_fileids: If not None, only include revisions
232
239
      affecting the specified files, rather than all revisions.
233
240
 
234
241
    :param start_revision: If not None, only generate
251
258
`
252
259
    :param delta_type: Either 'full', 'partial' or None.
253
260
      'full' means generate the complete delta - adds/deletes/modifies/etc;
254
 
      'partial' means filter the delta using specific_files;
 
261
      'partial' means filter the delta using specific_fileids;
255
262
      None means do not generate any delta.
256
263
 
257
264
    :param diff_type: Either 'full', 'partial' or None.
258
265
      'full' means generate the complete diff - adds/deletes/modifies/etc;
259
 
      'partial' means filter the diff using specific_files;
 
266
      'partial' means filter the diff using specific_fileids;
260
267
      None means do not generate any diff.
261
268
 
262
269
    :param _match_using_deltas: a private parameter controlling the
263
 
      algorithm used for matching specific_files. This parameter
 
270
      algorithm used for matching specific_fileids. This parameter
264
271
      may be removed in the future so breezy client code should NOT
265
272
      use it.
266
273
 
288
295
            match = {'message': [message_search]}
289
296
    return {
290
297
        'direction': direction,
291
 
        'specific_files': specific_files,
 
298
        'specific_fileids': specific_fileids,
292
299
        'start_revision': start_revision,
293
300
        'end_revision': end_revision,
294
301
        'limit': limit,
400
407
            for lr in generator.iter_log_revisions():
401
408
                lf.log_revision(lr)
402
409
        except errors.GhostRevisionUnusableHere:
403
 
            raise errors.CommandError(
 
410
            raise errors.BzrCommandError(
404
411
                gettext('Further revision history missing.'))
405
412
        lf.show_advice()
406
413
 
409
416
 
410
417
        Subclasses may wish to override this.
411
418
        """
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()
 
419
        return _DefaultLogGenerator(branch, rqst)
479
420
 
480
421
 
481
422
class _StartNotLinearAncestor(Exception):
485
426
class _DefaultLogGenerator(LogGenerator):
486
427
    """The default generator of log revisions."""
487
428
 
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):
 
429
    def __init__(self, branch, rqst):
495
430
        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():
 
431
        self.rqst = rqst
 
432
        if rqst.get('generate_tags') and branch.supports_tags():
510
433
            self.rev_tag_dict = branch.tags.get_reverse_tag_dict()
511
434
        else:
512
435
            self.rev_tag_dict = {}
516
439
 
517
440
        :return: An iterator yielding LogRevision objects.
518
441
        """
 
442
        rqst = self.rqst
 
443
        levels = rqst.get('levels')
 
444
        limit = rqst.get('limit')
 
445
        diff_type = rqst.get('diff_type')
 
446
        show_signature = rqst.get('signature')
 
447
        omit_merges = rqst.get('omit_merges')
519
448
        log_count = 0
520
449
        revision_iterator = self._create_log_revision_iterator()
521
450
        for revs in revision_iterator:
522
451
            for (rev_id, revno, merge_depth), rev, delta in revs:
523
452
                # 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):
 
453
                if (levels != 0 and merge_depth is not None and
 
454
                        merge_depth >= levels):
526
455
                    continue
527
 
                if self.omit_merges and len(rev.parent_ids) > 1:
 
456
                if omit_merges and len(rev.parent_ids) > 1:
528
457
                    continue
529
458
                if rev is None:
530
459
                    raise errors.GhostRevisionUnusableHere(rev_id)
531
 
                if self.diff_type is None:
 
460
                if diff_type is None:
532
461
                    diff = None
533
462
                else:
534
 
                    diff = _format_diff(
535
 
                        self.branch, rev, self.diff_type,
536
 
                        self.specific_files)
537
 
                if self.show_signature:
 
463
                    diff = self._format_diff(rev, rev_id, diff_type)
 
464
                if show_signature:
538
465
                    signature = format_signature_validity(rev_id, self.branch)
539
466
                else:
540
467
                    signature = None
541
468
                yield LogRevision(
542
469
                    rev, revno, merge_depth, delta,
543
470
                    self.rev_tag_dict.get(rev_id), diff, signature)
544
 
                if self.limit:
 
471
                if limit:
545
472
                    log_count += 1
546
 
                    if log_count >= self.limit:
 
473
                    if log_count >= limit:
547
474
                        return
548
475
 
 
476
    def _format_diff(self, rev, rev_id, diff_type):
 
477
        repo = self.branch.repository
 
478
        if len(rev.parent_ids) == 0:
 
479
            ancestor_id = _mod_revision.NULL_REVISION
 
480
        else:
 
481
            ancestor_id = rev.parent_ids[0]
 
482
        tree_1 = repo.revision_tree(ancestor_id)
 
483
        tree_2 = repo.revision_tree(rev_id)
 
484
        file_ids = self.rqst.get('specific_fileids')
 
485
        if diff_type == 'partial' and file_ids is not None:
 
486
            specific_files = [tree_2.id2path(id) for id in file_ids]
 
487
        else:
 
488
            specific_files = None
 
489
        s = BytesIO()
 
490
        path_encoding = get_diff_header_encoding()
 
491
        diff.show_diff_trees(tree_1, tree_2, s, specific_files, old_label='',
 
492
                             new_label='', path_encoding=path_encoding)
 
493
        return s.getvalue()
 
494
 
549
495
    def _create_log_revision_iterator(self):
550
496
        """Create a revision iterator for log.
551
497
 
552
498
        :return: An iterator over lists of ((rev_id, revno, merge_depth), rev,
553
499
            delta).
554
500
        """
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)
 
501
        self.start_rev_id, self.end_rev_id = _get_revision_limits(
 
502
            self.branch, self.rqst.get('start_revision'),
 
503
            self.rqst.get('end_revision'))
 
504
        if self.rqst.get('_match_using_deltas'):
 
505
            return self._log_revision_iterator_using_delta_matching()
568
506
        else:
569
507
            # We're using the per-file-graph algorithm. This scales really
570
508
            # well but only makes sense if there is a single file and it's
571
509
            # not a directory
572
 
            file_count = len(self.specific_files)
 
510
            file_count = len(self.rqst.get('specific_fileids'))
573
511
            if file_count != 1:
574
512
                raise errors.BzrError(
575
513
                    "illegal LogRequest: must match-using-deltas "
576
514
                    "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
 
                )
 
515
            return self._log_revision_iterator_using_per_file_graph()
 
516
 
 
517
    def _log_revision_iterator_using_delta_matching(self):
 
518
        # Get the base revisions, filtering by the revision range
 
519
        rqst = self.rqst
 
520
        generate_merge_revisions = rqst.get('levels') != 1
 
521
        delayed_graph_generation = not rqst.get('specific_fileids') and (
 
522
            rqst.get('limit') or self.start_rev_id or self.end_rev_id)
 
523
        view_revisions = _calc_view_revisions(
 
524
            self.branch, self.start_rev_id, self.end_rev_id,
 
525
            rqst.get('direction'),
 
526
            generate_merge_revisions=generate_merge_revisions,
 
527
            delayed_graph_generation=delayed_graph_generation,
 
528
            exclude_common_ancestry=rqst.get('exclude_common_ancestry'))
 
529
 
 
530
        # Apply the other filters
 
531
        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'))
 
535
 
 
536
    def _log_revision_iterator_using_per_file_graph(self):
 
537
        # Get the base revisions, filtering by the revision range.
 
538
        # Note that we always generate the merge revisions because
 
539
        # filter_revisions_touching_file_id() requires them ...
 
540
        rqst = self.rqst
 
541
        view_revisions = _calc_view_revisions(
 
542
            self.branch, self.start_rev_id, self.end_rev_id,
 
543
            rqst.get('direction'), generate_merge_revisions=True,
 
544
            exclude_common_ancestry=rqst.get('exclude_common_ancestry'))
 
545
        if not isinstance(view_revisions, list):
 
546
            view_revisions = list(view_revisions)
 
547
        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)
 
551
        return make_log_rev_iterator(self.branch, view_revisions,
 
552
                                     rqst.get('delta_type'), rqst.get('match'))
587
553
 
588
554
 
589
555
def _calc_view_revisions(branch, start_rev_id, end_rev_id, direction,
597
563
             a list of the same tuples.
598
564
    """
599
565
    if (exclude_common_ancestry and start_rev_id == end_rev_id):
600
 
        raise errors.CommandError(gettext(
 
566
        raise errors.BzrCommandError(gettext(
601
567
            '--exclude-common-ancestry requires two different revisions'))
602
568
    if direction not in ('reverse', 'forward'):
603
569
        raise ValueError(gettext('invalid direction %r') % direction)
688
654
        except _StartNotLinearAncestor:
689
655
            # A merge was never detected so the lower revision limit can't
690
656
            # be nested down somewhere
691
 
            raise errors.CommandError(gettext('Start revision not found in'
 
657
            raise errors.BzrCommandError(gettext('Start revision not found in'
692
658
                                                 ' history of end revision.'))
693
659
 
694
660
    # We exit the loop above because we encounter a revision with merges, from
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).
912
878
        # with custom parameters. This will do for now. IGC 20090127
913
879
        if adapter == _make_delta_filter:
914
880
            log_rev_iterator = adapter(
915
 
                branch, generate_delta, search, log_rev_iterator, files,
 
881
                branch, generate_delta, search, log_rev_iterator, file_ids,
916
882
                direction)
917
883
        else:
918
884
            log_rev_iterator = adapter(
969
935
 
970
936
 
971
937
def _make_delta_filter(branch, generate_delta, search, log_rev_iterator,
972
 
                       files=None, direction='reverse'):
 
938
                       fileids=None, direction='reverse'):
973
939
    """Add revision deltas to a log iterator if needed.
974
940
 
975
941
    :param branch: The branch being logged.
978
944
    :param search: A user text search string.
979
945
    :param log_rev_iterator: An input iterator containing all revisions that
980
946
        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.
 
947
    :param fileids: If non empty, only revisions matching one or more of
 
948
      the file-ids are to be kept.
983
949
    :param direction: the direction in which view_revisions is sorted
984
950
    :return: An iterator over lists of ((rev_id, revno, merge_depth), rev,
985
951
        delta).
986
952
    """
987
 
    if not generate_delta and not files:
 
953
    if not generate_delta and not fileids:
988
954
        return log_rev_iterator
989
955
    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,
 
956
                            generate_delta, fileids, direction)
 
957
 
 
958
 
 
959
def _generate_deltas(repository, log_rev_iterator, delta_type, fileids,
994
960
                     direction):
995
961
    """Create deltas for each batch of revisions in log_rev_iterator.
996
962
 
997
963
    If we're only generating deltas for the sake of filtering against
998
 
    files, we stop generating deltas once all files reach the
 
964
    file-ids, we stop generating deltas once all file-ids reach the
999
965
    appropriate life-cycle point. If we're receiving data newest to
1000
966
    oldest, then that life-cycle point is 'add', otherwise it's 'remove'.
1001
967
    """
1002
 
    check_files = files is not None and len(files) > 0
1003
 
    if check_files:
1004
 
        file_set = set(files)
 
968
    check_fileids = fileids is not None and len(fileids) > 0
 
969
    if check_fileids:
 
970
        fileid_set = set(fileids)
1005
971
        if direction == 'reverse':
1006
972
            stop_on = 'add'
1007
973
        else:
1008
974
            stop_on = 'remove'
1009
975
    else:
1010
 
        file_set = None
 
976
        fileid_set = None
1011
977
    for revs in log_rev_iterator:
1012
 
        # If we were matching against files and we've run out,
 
978
        # If we were matching against fileids and we've run out,
1013
979
        # there's nothing left to do
1014
 
        if check_files and not file_set:
 
980
        if check_fileids and not fileid_set:
1015
981
            return
1016
982
        revisions = [rev[1] for rev in revs]
1017
983
        new_revs = []
1018
 
        if delta_type == 'full' and not check_files:
1019
 
            deltas = repository.get_revision_deltas(revisions)
 
984
        if delta_type == 'full' and not check_fileids:
 
985
            deltas = repository.get_deltas_for_revisions(revisions)
1020
986
            for rev, delta in zip(revs, deltas):
1021
987
                new_revs.append((rev[0], rev[1], delta))
1022
988
        else:
1023
 
            deltas = repository.get_revision_deltas(
1024
 
                revisions, specific_files=file_set)
 
989
            deltas = repository.get_deltas_for_revisions(revisions, fileid_set)
1025
990
            for rev, delta in zip(revs, deltas):
1026
 
                if check_files:
 
991
                if check_fileids:
1027
992
                    if delta is None or not delta.has_changed():
1028
993
                        continue
1029
994
                    else:
1030
 
                        _update_files(delta, file_set, stop_on)
 
995
                        _update_fileids(delta, fileid_set, stop_on)
1031
996
                        if delta_type is None:
1032
997
                            delta = None
1033
998
                        elif delta_type == 'full':
1044
1009
        yield new_revs
1045
1010
 
1046
1011
 
1047
 
def _update_files(delta, files, stop_on):
1048
 
    """Update the set of files to search based on file lifecycle events.
 
1012
def _update_fileids(delta, fileids, stop_on):
 
1013
    """Update the set of file-ids to search based on file lifecycle events.
1049
1014
 
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
 
1015
    :param fileids: a set of fileids to update
 
1016
    :param stop_on: either 'add' or 'remove' - take file-ids out of the
 
1017
      fileids set once their add or remove entry is detected respectively
1053
1018
    """
1054
1019
    if stop_on == 'add':
1055
 
        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]):])
 
1020
        for item in delta.added + delta.copied:
 
1021
            if item.file_id in fileids:
 
1022
                fileids.remove(item.file_id)
1067
1023
    elif stop_on == 'delete':
1068
1024
        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]):])
 
1025
            if item.file_id in fileids:
 
1026
                fileids.remove(item.file_id)
1080
1027
 
1081
1028
 
1082
1029
def _make_revision_objects(branch, generate_delta, search, log_rev_iterator):
1123
1070
def _get_revision_limits(branch, start_revision, end_revision):
1124
1071
    """Get and check revision limits.
1125
1072
 
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
 
1073
    :param  branch: The branch containing the revisions.
 
1074
 
 
1075
    :param  start_revision: The first revision to be logged.
 
1076
            but for merge revision support a RevisionInfo is expected.
 
1077
 
 
1078
    :param  end_revision: The last revision to be logged.
 
1079
            For backwards compatibility this may be a mainline integer revno,
 
1080
            but for merge revision support a RevisionInfo is expected.
1131
1081
 
1132
1082
    :return: (start_rev_id, end_rev_id) tuple.
1133
1083
    """
1152
1102
    if branch.last_revision() != _mod_revision.NULL_REVISION:
1153
1103
        if (start_rev_id == _mod_revision.NULL_REVISION
1154
1104
                or end_rev_id == _mod_revision.NULL_REVISION):
1155
 
            raise errors.CommandError(
 
1105
            raise errors.BzrCommandError(
1156
1106
                gettext('Logging revision 0 is invalid.'))
1157
1107
        if end_revno is not None and start_revno > end_revno:
1158
 
            raise errors.CommandError(
 
1108
            raise errors.BzrCommandError(
1159
1109
                gettext("Start revision must be older than the end revision."))
1160
1110
    return (start_rev_id, end_rev_id)
1161
1111
 
1211
1161
 
1212
1162
    if ((start_rev_id == _mod_revision.NULL_REVISION)
1213
1163
            or (end_rev_id == _mod_revision.NULL_REVISION)):
1214
 
        raise errors.CommandError(gettext('Logging revision 0 is invalid.'))
 
1164
        raise errors.BzrCommandError(gettext('Logging revision 0 is invalid.'))
1215
1165
    if start_revno > end_revno:
1216
 
        raise errors.CommandError(gettext("Start revision must be older "
 
1166
        raise errors.BzrCommandError(gettext("Start revision must be older "
1217
1167
                                             "than the end revision."))
1218
1168
 
1219
1169
    if end_revno < start_revno:
1243
1193
    return mainline_revs, rev_nos, start_rev_id, end_rev_id
1244
1194
 
1245
1195
 
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.
 
1196
def _filter_revisions_touching_file_id(branch, file_id, view_revisions,
 
1197
                                       include_merges=True):
 
1198
    r"""Return the list of revision ids which touch a given file id.
1249
1199
 
1250
1200
    The function filters view_revisions and returns a subset.
1251
 
    This includes the revisions which directly change the path,
 
1201
    This includes the revisions which directly change the file id,
1252
1202
    and the revisions which merge these changes. So if the
1253
1203
    revision graph is::
1254
1204
 
1271
1221
 
1272
1222
    :param branch: The branch where we can get text revision information.
1273
1223
 
1274
 
    :param path: Filter out revisions that do not touch path.
 
1224
    :param file_id: Filter out revisions that do not touch file_id.
1275
1225
 
1276
1226
    :param view_revisions: A list of (revision_id, dotted_revno, merge_depth)
1277
1227
        tuples. This is the list of revisions which will be filtered. It is
1285
1235
    # Lookup all possible text keys to determine which ones actually modified
1286
1236
    # the file.
1287
1237
    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
1238
    get_parent_map = graph.get_parent_map
1291
1239
    text_keys = [(file_id, rev_id) for rev_id, revno, depth in view_revisions]
1292
1240
    next_keys = None
1913
1861
    try:
1914
1862
        return log_formatter_registry.make_formatter(name, *args, **kwargs)
1915
1863
    except KeyError:
1916
 
        raise errors.CommandError(
 
1864
        raise errors.BzrCommandError(
1917
1865
            gettext("unknown log formatter: %r") % name)
1918
1866
 
1919
1867
 
1994
1942
                 verbose=False,
1995
1943
                 direction='forward',
1996
1944
                 start_revision=base_idx + 1,
1997
 
                 end_revision=len(new_rh))
 
1945
                 end_revision=len(new_rh),
 
1946
                 search=None)
1998
1947
 
1999
1948
 
2000
1949
def get_history_change(old_revision_id, new_revision_id, repository):
2093
2042
 
2094
2043
 
2095
2044
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.
 
2045
    """Find file-ids and kinds given a list of files and a revision range.
2097
2046
 
2098
2047
    We search for files at the end of the range. If not found there,
2099
2048
    we try the start of the range.
2105
2054
    :param exit_stack: When the branch returned is read locked,
2106
2055
      an unlock call will be queued to the exit stack.
2107
2056
    :return: (branch, info_list, start_rev_info, end_rev_info) where
2108
 
      info_list is a list of (relative_path, found, kind) tuples where
 
2057
      info_list is a list of (relative_path, file_id, kind) tuples where
2109
2058
      kind is one of values 'directory', 'file', 'symlink', 'tree-reference'.
2110
2059
      branch will be read-locked.
2111
2060
    """
2135
2084
            tree = b.basis_tree()
2136
2085
        tree1 = None
2137
2086
        for fp in relpaths:
2138
 
            kind = _get_kind_for_file(tree, fp)
2139
 
            if not kind:
 
2087
            file_id = tree.path2id(fp)
 
2088
            kind = _get_kind_for_file_id(tree, fp, file_id)
 
2089
            if file_id is None:
2140
2090
                # go back to when time began
2141
2091
                if tree1 is None:
2142
2092
                    try:
2143
2093
                        rev1 = b.get_rev_id(1)
2144
2094
                    except errors.NoSuchRevision:
2145
2095
                        # No history at all
 
2096
                        file_id = None
2146
2097
                        kind = None
2147
2098
                    else:
2148
2099
                        tree1 = b.repository.revision_tree(rev1)
2149
2100
                if tree1:
2150
 
                    kind = _get_kind_for_file(tree1, fp)
2151
 
            info_list.append((fp, kind))
 
2101
                    file_id = tree1.path2id(fp)
 
2102
                    kind = _get_kind_for_file_id(tree1, fp, file_id)
 
2103
            info_list.append((fp, file_id, kind))
2152
2104
 
2153
2105
    elif start_rev_info == end_rev_info:
2154
2106
        # One revision given - file must exist in it
2155
2107
        tree = b.repository.revision_tree(end_rev_info.rev_id)
2156
2108
        for fp in relpaths:
2157
 
            kind = _get_kind_for_file(tree, fp)
2158
 
            info_list.append((fp, kind))
 
2109
            file_id = tree.path2id(fp)
 
2110
            kind = _get_kind_for_file_id(tree, fp, file_id)
 
2111
            info_list.append((fp, file_id, kind))
2159
2112
 
2160
2113
    else:
2161
2114
        # Revision range given. Get the file-id from the end tree.
2167
2120
            tree = b.repository.revision_tree(rev_id)
2168
2121
        tree1 = None
2169
2122
        for fp in relpaths:
2170
 
            kind = _get_kind_for_file(tree, fp)
2171
 
            if not kind:
 
2123
            file_id = tree.path2id(fp)
 
2124
            kind = _get_kind_for_file_id(tree, fp, file_id)
 
2125
            if file_id is None:
2172
2126
                if tree1 is None:
2173
2127
                    rev_id = start_rev_info.rev_id
2174
2128
                    if rev_id is None:
2176
2130
                        tree1 = b.repository.revision_tree(rev1)
2177
2131
                    else:
2178
2132
                        tree1 = b.repository.revision_tree(rev_id)
2179
 
                kind = _get_kind_for_file(tree1, fp)
2180
 
            info_list.append((fp, kind))
 
2133
                file_id = tree1.path2id(fp)
 
2134
                kind = _get_kind_for_file_id(tree1, fp, file_id)
 
2135
            info_list.append((fp, file_id, kind))
2181
2136
    return b, info_list, start_rev_info, end_rev_info
2182
2137
 
2183
2138
 
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
 
2139
def _get_kind_for_file_id(tree, path, file_id):
 
2140
    """Return the kind of a file-id or None if it doesn't exist."""
 
2141
    if file_id is not None:
 
2142
        return tree.kind(path)
 
2143
    else:
 
2144
        return None
2191
2145
 
2192
2146
 
2193
2147
properties_handler_registry = registry.Registry()