/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: 2019-03-05 07:32:38 UTC
  • mto: (7290.1.21 work)
  • mto: This revision was merged to the branch mainline in revision 7311.
  • Revision ID: jelmer@jelmer.uk-20190305073238-zlqn981opwnqsmzi
Add appveyor configuration.

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 find_previous_path
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
 
107
109
    revno = len(history)
108
110
    for revision_id in history:
109
111
        this_tree = repository.revision_tree(revision_id)
110
 
        this_intertree = InterTree.get(this_tree, last_tree)
111
 
        this_path = this_intertree.find_source_path(last_path)
 
112
        this_path = find_previous_path(last_tree, this_tree, last_path)
112
113
 
113
114
        # now we know how it was last time, and how it is in this revision.
114
115
        # are those two states effectively the same or not?
135
136
 
136
137
def show_log(branch,
137
138
             lf,
 
139
             specific_fileid=None,
138
140
             verbose=False,
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):
151
154
 
152
155
    :param lf: The LogFormatter object showing the output.
153
156
 
 
157
    :param specific_fileid: If not None, list only the commits affecting the
 
158
        specified file, rather than all commits.
 
159
 
154
160
    :param verbose: If True show added/changed/deleted/renamed files.
155
161
 
156
162
    :param direction: 'reverse' (default) is latest to earliest; 'forward' is
160
166
 
161
167
    :param end_revision: If not None, only show revisions <= end_revision
162
168
 
 
169
    :param search: If not None, only show revisions with matching commit
 
170
        messages
 
171
 
163
172
    :param limit: If set, shows only 'limit' revisions, all revisions are shown
164
173
        if None or 0.
165
174
 
168
177
    :param match: Dictionary of search lists to use when matching revision
169
178
      properties.
170
179
    """
 
180
    # Convert old-style parameters to new-style parameters
 
181
    if specific_fileid is not None:
 
182
        file_ids = [specific_fileid]
 
183
    else:
 
184
        file_ids = None
171
185
    if verbose:
172
 
        delta_type = 'full'
 
186
        if file_ids:
 
187
            delta_type = 'partial'
 
188
        else:
 
189
            delta_type = 'full'
173
190
    else:
174
191
        delta_type = None
175
192
    if show_diff:
176
 
        diff_type = 'full'
 
193
        if file_ids:
 
194
            diff_type = 'partial'
 
195
        else:
 
196
            diff_type = 'full'
177
197
    else:
178
198
        diff_type = None
179
199
 
180
200
    if isinstance(start_revision, int):
181
201
        try:
182
202
            start_revision = revisionspec.RevisionInfo(branch, start_revision)
183
 
        except (errors.NoSuchRevision, errors.RevnoOutOfBounds):
 
203
        except errors.NoSuchRevision:
184
204
            raise errors.InvalidRevisionNumber(start_revision)
185
205
 
186
206
    if isinstance(end_revision, int):
187
207
        try:
188
208
            end_revision = revisionspec.RevisionInfo(branch, end_revision)
189
 
        except (errors.NoSuchRevision, errors.RevnoOutOfBounds):
 
209
        except errors.NoSuchRevision:
190
210
            raise errors.InvalidRevisionNumber(end_revision)
191
211
 
192
212
    if end_revision is not None and end_revision.revno == 0:
193
213
        raise errors.InvalidRevisionNumber(end_revision.revno)
194
214
 
195
215
    # Build the request and execute it
196
 
    rqst = make_log_request_dict(
197
 
        direction=direction,
198
 
        start_revision=start_revision, end_revision=end_revision,
199
 
        limit=limit, delta_type=delta_type, diff_type=diff_type)
 
216
    rqst = make_log_request_dict(direction=direction, specific_fileids=file_ids,
 
217
                                 start_revision=start_revision, end_revision=end_revision,
 
218
                                 limit=limit, message_search=search,
 
219
                                 delta_type=delta_type, diff_type=diff_type)
200
220
    Logger(branch, rqst).show(lf)
201
221
 
202
222
 
211
231
    }
212
232
 
213
233
 
214
 
def make_log_request_dict(direction='reverse', specific_files=None,
 
234
def make_log_request_dict(direction='reverse', specific_fileids=None,
215
235
                          start_revision=None, end_revision=None, limit=None,
216
236
                          message_search=None, levels=None, generate_tags=True,
217
237
                          delta_type=None,
228
248
    :param direction: 'reverse' (default) is latest to earliest;
229
249
      'forward' is earliest to latest.
230
250
 
231
 
    :param specific_files: If not None, only include revisions
 
251
    :param specific_fileids: If not None, only include revisions
232
252
      affecting the specified files, rather than all revisions.
233
253
 
234
254
    :param start_revision: If not None, only generate
251
271
`
252
272
    :param delta_type: Either 'full', 'partial' or None.
253
273
      'full' means generate the complete delta - adds/deletes/modifies/etc;
254
 
      'partial' means filter the delta using specific_files;
 
274
      'partial' means filter the delta using specific_fileids;
255
275
      None means do not generate any delta.
256
276
 
257
277
    :param diff_type: Either 'full', 'partial' or None.
258
278
      'full' means generate the complete diff - adds/deletes/modifies/etc;
259
 
      'partial' means filter the diff using specific_files;
 
279
      'partial' means filter the diff using specific_fileids;
260
280
      None means do not generate any diff.
261
281
 
262
282
    :param _match_using_deltas: a private parameter controlling the
263
 
      algorithm used for matching specific_files. This parameter
 
283
      algorithm used for matching specific_fileids. This parameter
264
284
      may be removed in the future so breezy client code should NOT
265
285
      use it.
266
286
 
288
308
            match = {'message': [message_search]}
289
309
    return {
290
310
        'direction': direction,
291
 
        'specific_files': specific_files,
 
311
        'specific_fileids': specific_fileids,
292
312
        'start_revision': start_revision,
293
313
        'end_revision': end_revision,
294
314
        'limit': limit,
400
420
            for lr in generator.iter_log_revisions():
401
421
                lf.log_revision(lr)
402
422
        except errors.GhostRevisionUnusableHere:
403
 
            raise errors.CommandError(
 
423
            raise errors.BzrCommandError(
404
424
                gettext('Further revision history missing.'))
405
425
        lf.show_advice()
406
426
 
409
429
 
410
430
        Subclasses may wish to override this.
411
431
        """
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()
 
432
        return _DefaultLogGenerator(branch, rqst)
479
433
 
480
434
 
481
435
class _StartNotLinearAncestor(Exception):
485
439
class _DefaultLogGenerator(LogGenerator):
486
440
    """The default generator of log revisions."""
487
441
 
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):
 
442
    def __init__(self, branch, rqst):
495
443
        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():
 
444
        self.rqst = rqst
 
445
        if rqst.get('generate_tags') and branch.supports_tags():
510
446
            self.rev_tag_dict = branch.tags.get_reverse_tag_dict()
511
447
        else:
512
448
            self.rev_tag_dict = {}
516
452
 
517
453
        :return: An iterator yielding LogRevision objects.
518
454
        """
 
455
        rqst = self.rqst
 
456
        levels = rqst.get('levels')
 
457
        limit = rqst.get('limit')
 
458
        diff_type = rqst.get('diff_type')
 
459
        show_signature = rqst.get('signature')
 
460
        omit_merges = rqst.get('omit_merges')
519
461
        log_count = 0
520
462
        revision_iterator = self._create_log_revision_iterator()
521
463
        for revs in revision_iterator:
522
464
            for (rev_id, revno, merge_depth), rev, delta in revs:
523
465
                # 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):
 
466
                if (levels != 0 and merge_depth is not None and
 
467
                        merge_depth >= levels):
526
468
                    continue
527
 
                if self.omit_merges and len(rev.parent_ids) > 1:
 
469
                if omit_merges and len(rev.parent_ids) > 1:
528
470
                    continue
529
471
                if rev is None:
530
472
                    raise errors.GhostRevisionUnusableHere(rev_id)
531
 
                if self.diff_type is None:
 
473
                if diff_type is None:
532
474
                    diff = None
533
475
                else:
534
 
                    diff = _format_diff(
535
 
                        self.branch, rev, self.diff_type,
536
 
                        self.specific_files)
537
 
                if self.show_signature:
 
476
                    diff = self._format_diff(rev, rev_id, diff_type)
 
477
                if show_signature:
538
478
                    signature = format_signature_validity(rev_id, self.branch)
539
479
                else:
540
480
                    signature = None
541
481
                yield LogRevision(
542
482
                    rev, revno, merge_depth, delta,
543
483
                    self.rev_tag_dict.get(rev_id), diff, signature)
544
 
                if self.limit:
 
484
                if limit:
545
485
                    log_count += 1
546
 
                    if log_count >= self.limit:
 
486
                    if log_count >= limit:
547
487
                        return
548
488
 
 
489
    def _format_diff(self, rev, rev_id, diff_type):
 
490
        repo = self.branch.repository
 
491
        if len(rev.parent_ids) == 0:
 
492
            ancestor_id = _mod_revision.NULL_REVISION
 
493
        else:
 
494
            ancestor_id = rev.parent_ids[0]
 
495
        tree_1 = repo.revision_tree(ancestor_id)
 
496
        tree_2 = repo.revision_tree(rev_id)
 
497
        file_ids = self.rqst.get('specific_fileids')
 
498
        if diff_type == 'partial' and file_ids is not None:
 
499
            specific_files = [tree_2.id2path(id) for id in file_ids]
 
500
        else:
 
501
            specific_files = None
 
502
        s = BytesIO()
 
503
        path_encoding = get_diff_header_encoding()
 
504
        diff.show_diff_trees(tree_1, tree_2, s, specific_files, old_label='',
 
505
                             new_label='', path_encoding=path_encoding)
 
506
        return s.getvalue()
 
507
 
549
508
    def _create_log_revision_iterator(self):
550
509
        """Create a revision iterator for log.
551
510
 
552
511
        :return: An iterator over lists of ((rev_id, revno, merge_depth), rev,
553
512
            delta).
554
513
        """
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)
 
514
        self.start_rev_id, self.end_rev_id = _get_revision_limits(
 
515
            self.branch, self.rqst.get('start_revision'),
 
516
            self.rqst.get('end_revision'))
 
517
        if self.rqst.get('_match_using_deltas'):
 
518
            return self._log_revision_iterator_using_delta_matching()
568
519
        else:
569
520
            # We're using the per-file-graph algorithm. This scales really
570
521
            # well but only makes sense if there is a single file and it's
571
522
            # not a directory
572
 
            file_count = len(self.specific_files)
 
523
            file_count = len(self.rqst.get('specific_fileids'))
573
524
            if file_count != 1:
574
525
                raise errors.BzrError(
575
526
                    "illegal LogRequest: must match-using-deltas "
576
527
                    "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
 
                )
 
528
            return self._log_revision_iterator_using_per_file_graph()
 
529
 
 
530
    def _log_revision_iterator_using_delta_matching(self):
 
531
        # Get the base revisions, filtering by the revision range
 
532
        rqst = self.rqst
 
533
        generate_merge_revisions = rqst.get('levels') != 1
 
534
        delayed_graph_generation = not rqst.get('specific_fileids') and (
 
535
            rqst.get('limit') or self.start_rev_id or self.end_rev_id)
 
536
        view_revisions = _calc_view_revisions(
 
537
            self.branch, self.start_rev_id, self.end_rev_id,
 
538
            rqst.get('direction'),
 
539
            generate_merge_revisions=generate_merge_revisions,
 
540
            delayed_graph_generation=delayed_graph_generation,
 
541
            exclude_common_ancestry=rqst.get('exclude_common_ancestry'))
 
542
 
 
543
        # Apply the other filters
 
544
        return make_log_rev_iterator(self.branch, view_revisions,
 
545
                                     rqst.get('delta_type'), rqst.get('match'),
 
546
                                     file_ids=rqst.get('specific_fileids'),
 
547
                                     direction=rqst.get('direction'))
 
548
 
 
549
    def _log_revision_iterator_using_per_file_graph(self):
 
550
        # Get the base revisions, filtering by the revision range.
 
551
        # Note that we always generate the merge revisions because
 
552
        # filter_revisions_touching_file_id() requires them ...
 
553
        rqst = self.rqst
 
554
        view_revisions = _calc_view_revisions(
 
555
            self.branch, self.start_rev_id, self.end_rev_id,
 
556
            rqst.get('direction'), generate_merge_revisions=True,
 
557
            exclude_common_ancestry=rqst.get('exclude_common_ancestry'))
 
558
        if not isinstance(view_revisions, list):
 
559
            view_revisions = list(view_revisions)
 
560
        view_revisions = _filter_revisions_touching_file_id(self.branch,
 
561
                                                            rqst.get('specific_fileids')[
 
562
                                                                0], view_revisions,
 
563
                                                            include_merges=rqst.get('levels') != 1)
 
564
        return make_log_rev_iterator(self.branch, view_revisions,
 
565
                                     rqst.get('delta_type'), rqst.get('match'))
587
566
 
588
567
 
589
568
def _calc_view_revisions(branch, start_rev_id, end_rev_id, direction,
597
576
             a list of the same tuples.
598
577
    """
599
578
    if (exclude_common_ancestry and start_rev_id == end_rev_id):
600
 
        raise errors.CommandError(gettext(
 
579
        raise errors.BzrCommandError(gettext(
601
580
            '--exclude-common-ancestry requires two different revisions'))
602
581
    if direction not in ('reverse', 'forward'):
603
582
        raise ValueError(gettext('invalid direction %r') % direction)
688
667
        except _StartNotLinearAncestor:
689
668
            # A merge was never detected so the lower revision limit can't
690
669
            # be nested down somewhere
691
 
            raise errors.CommandError(gettext('Start revision not found in'
 
670
            raise errors.BzrCommandError(gettext('Start revision not found in'
692
671
                                                 ' history of end revision.'))
693
672
 
694
673
    # We exit the loop above because we encounter a revision with merges, from
768
747
    repo = branch.repository
769
748
    graph = repo.get_graph()
770
749
    if start_rev_id is None and end_rev_id is None:
771
 
        if branch._format.stores_revno() or \
772
 
                config.GlobalStack().get('calculate_revnos'):
773
 
            try:
774
 
                br_revno, br_rev_id = branch.last_revision_info()
775
 
            except errors.GhostRevisionsHaveNoRevno:
776
 
                br_rev_id = branch.last_revision()
777
 
                cur_revno = None
778
 
            else:
779
 
                cur_revno = br_revno
780
 
        else:
 
750
        try:
 
751
            br_revno, br_rev_id = branch.last_revision_info()
 
752
        except errors.GhostRevisionsHaveNoRevno:
781
753
            br_rev_id = branch.last_revision()
782
754
            cur_revno = None
783
 
 
 
755
        else:
 
756
            cur_revno = br_revno
784
757
        graph_iter = graph.iter_lefthand_ancestry(br_rev_id,
785
758
                                                  (_mod_revision.NULL_REVISION,))
786
759
        while True:
881
854
 
882
855
 
883
856
def make_log_rev_iterator(branch, view_revisions, generate_delta, search,
884
 
                          files=None, direction='reverse'):
 
857
                          file_ids=None, direction='reverse'):
885
858
    """Create a revision iterator for log.
886
859
 
887
860
    :param branch: The branch being logged.
889
862
    :param generate_delta: Whether to generate a delta for each revision.
890
863
      Permitted values are None, 'full' and 'partial'.
891
864
    :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.
 
865
    :param file_ids: If non empty, only revisions matching one or more of
 
866
      the file-ids are to be kept.
894
867
    :param direction: the direction in which view_revisions is sorted
895
868
    :return: An iterator over lists of ((rev_id, revno, merge_depth), rev,
896
869
        delta).
912
885
        # with custom parameters. This will do for now. IGC 20090127
913
886
        if adapter == _make_delta_filter:
914
887
            log_rev_iterator = adapter(
915
 
                branch, generate_delta, search, log_rev_iterator, files,
 
888
                branch, generate_delta, search, log_rev_iterator, file_ids,
916
889
                direction)
917
890
        else:
918
891
            log_rev_iterator = adapter(
969
942
 
970
943
 
971
944
def _make_delta_filter(branch, generate_delta, search, log_rev_iterator,
972
 
                       files=None, direction='reverse'):
 
945
                       fileids=None, direction='reverse'):
973
946
    """Add revision deltas to a log iterator if needed.
974
947
 
975
948
    :param branch: The branch being logged.
978
951
    :param search: A user text search string.
979
952
    :param log_rev_iterator: An input iterator containing all revisions that
980
953
        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.
 
954
    :param fileids: If non empty, only revisions matching one or more of
 
955
      the file-ids are to be kept.
983
956
    :param direction: the direction in which view_revisions is sorted
984
957
    :return: An iterator over lists of ((rev_id, revno, merge_depth), rev,
985
958
        delta).
986
959
    """
987
 
    if not generate_delta and not files:
 
960
    if not generate_delta and not fileids:
988
961
        return log_rev_iterator
989
962
    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,
 
963
                            generate_delta, fileids, direction)
 
964
 
 
965
 
 
966
def _generate_deltas(repository, log_rev_iterator, delta_type, fileids,
994
967
                     direction):
995
968
    """Create deltas for each batch of revisions in log_rev_iterator.
996
969
 
997
970
    If we're only generating deltas for the sake of filtering against
998
 
    files, we stop generating deltas once all files reach the
 
971
    file-ids, we stop generating deltas once all file-ids reach the
999
972
    appropriate life-cycle point. If we're receiving data newest to
1000
973
    oldest, then that life-cycle point is 'add', otherwise it's 'remove'.
1001
974
    """
1002
 
    check_files = files is not None and len(files) > 0
1003
 
    if check_files:
1004
 
        file_set = set(files)
 
975
    check_fileids = fileids is not None and len(fileids) > 0
 
976
    if check_fileids:
 
977
        fileid_set = set(fileids)
1005
978
        if direction == 'reverse':
1006
979
            stop_on = 'add'
1007
980
        else:
1008
981
            stop_on = 'remove'
1009
982
    else:
1010
 
        file_set = None
 
983
        fileid_set = None
1011
984
    for revs in log_rev_iterator:
1012
 
        # If we were matching against files and we've run out,
 
985
        # If we were matching against fileids and we've run out,
1013
986
        # there's nothing left to do
1014
 
        if check_files and not file_set:
 
987
        if check_fileids and not fileid_set:
1015
988
            return
1016
989
        revisions = [rev[1] for rev in revs]
1017
990
        new_revs = []
1018
 
        if delta_type == 'full' and not check_files:
1019
 
            deltas = repository.get_revision_deltas(revisions)
 
991
        if delta_type == 'full' and not check_fileids:
 
992
            deltas = repository.get_deltas_for_revisions(revisions)
1020
993
            for rev, delta in zip(revs, deltas):
1021
994
                new_revs.append((rev[0], rev[1], delta))
1022
995
        else:
1023
 
            deltas = repository.get_revision_deltas(
1024
 
                revisions, specific_files=file_set)
 
996
            deltas = repository.get_deltas_for_revisions(revisions, fileid_set)
1025
997
            for rev, delta in zip(revs, deltas):
1026
 
                if check_files:
 
998
                if check_fileids:
1027
999
                    if delta is None or not delta.has_changed():
1028
1000
                        continue
1029
1001
                    else:
1030
 
                        _update_files(delta, file_set, stop_on)
 
1002
                        _update_fileids(delta, fileid_set, stop_on)
1031
1003
                        if delta_type is None:
1032
1004
                            delta = None
1033
1005
                        elif delta_type == 'full':
1044
1016
        yield new_revs
1045
1017
 
1046
1018
 
1047
 
def _update_files(delta, files, stop_on):
1048
 
    """Update the set of files to search based on file lifecycle events.
 
1019
def _update_fileids(delta, fileids, stop_on):
 
1020
    """Update the set of file-ids to search based on file lifecycle events.
1049
1021
 
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
 
1022
    :param fileids: a set of fileids to update
 
1023
    :param stop_on: either 'add' or 'remove' - take file-ids out of the
 
1024
      fileids set once their add or remove entry is detected respectively
1053
1025
    """
1054
1026
    if stop_on == 'add':
1055
1027
        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]):])
 
1028
            if item[1] in fileids:
 
1029
                fileids.remove(item[1])
1067
1030
    elif stop_on == 'delete':
1068
1031
        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]):])
 
1032
            if item[1] in fileids:
 
1033
                fileids.remove(item[1])
1080
1034
 
1081
1035
 
1082
1036
def _make_revision_objects(branch, generate_delta, search, log_rev_iterator):
1123
1077
def _get_revision_limits(branch, start_revision, end_revision):
1124
1078
    """Get and check revision limits.
1125
1079
 
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
 
1080
    :param  branch: The branch containing the revisions.
 
1081
 
 
1082
    :param  start_revision: The first revision to be logged.
 
1083
            but for merge revision support a RevisionInfo is expected.
 
1084
 
 
1085
    :param  end_revision: The last revision to be logged.
 
1086
            For backwards compatibility this may be a mainline integer revno,
 
1087
            but for merge revision support a RevisionInfo is expected.
1131
1088
 
1132
1089
    :return: (start_rev_id, end_rev_id) tuple.
1133
1090
    """
1148
1105
            raise TypeError(start_revision)
1149
1106
        end_rev_id = end_revision.rev_id
1150
1107
        end_revno = end_revision.revno
 
1108
    if end_revno is None:
 
1109
        try:
 
1110
            end_revno = branch.revno()
 
1111
        except errors.GhostRevisionsHaveNoRevno:
 
1112
            end_revno = None
1151
1113
 
1152
1114
    if branch.last_revision() != _mod_revision.NULL_REVISION:
1153
1115
        if (start_rev_id == _mod_revision.NULL_REVISION
1154
1116
                or end_rev_id == _mod_revision.NULL_REVISION):
1155
 
            raise errors.CommandError(
 
1117
            raise errors.BzrCommandError(
1156
1118
                gettext('Logging revision 0 is invalid.'))
1157
1119
        if end_revno is not None and start_revno > end_revno:
1158
 
            raise errors.CommandError(
 
1120
            raise errors.BzrCommandError(
1159
1121
                gettext("Start revision must be older than the end revision."))
1160
1122
    return (start_rev_id, end_rev_id)
1161
1123
 
1211
1173
 
1212
1174
    if ((start_rev_id == _mod_revision.NULL_REVISION)
1213
1175
            or (end_rev_id == _mod_revision.NULL_REVISION)):
1214
 
        raise errors.CommandError(gettext('Logging revision 0 is invalid.'))
 
1176
        raise errors.BzrCommandError(gettext('Logging revision 0 is invalid.'))
1215
1177
    if start_revno > end_revno:
1216
 
        raise errors.CommandError(gettext("Start revision must be older "
 
1178
        raise errors.BzrCommandError(gettext("Start revision must be older "
1217
1179
                                             "than the end revision."))
1218
1180
 
1219
1181
    if end_revno < start_revno:
1243
1205
    return mainline_revs, rev_nos, start_rev_id, end_rev_id
1244
1206
 
1245
1207
 
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.
 
1208
def _filter_revisions_touching_file_id(branch, file_id, view_revisions,
 
1209
                                       include_merges=True):
 
1210
    r"""Return the list of revision ids which touch a given file id.
1249
1211
 
1250
1212
    The function filters view_revisions and returns a subset.
1251
 
    This includes the revisions which directly change the path,
 
1213
    This includes the revisions which directly change the file id,
1252
1214
    and the revisions which merge these changes. So if the
1253
1215
    revision graph is::
1254
1216
 
1271
1233
 
1272
1234
    :param branch: The branch where we can get text revision information.
1273
1235
 
1274
 
    :param path: Filter out revisions that do not touch path.
 
1236
    :param file_id: Filter out revisions that do not touch file_id.
1275
1237
 
1276
1238
    :param view_revisions: A list of (revision_id, dotted_revno, merge_depth)
1277
1239
        tuples. This is the list of revisions which will be filtered. It is
1285
1247
    # Lookup all possible text keys to determine which ones actually modified
1286
1248
    # the file.
1287
1249
    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
1250
    get_parent_map = graph.get_parent_map
1291
1251
    text_keys = [(file_id, rev_id) for rev_id, revno, depth in view_revisions]
1292
1252
    next_keys = None
1847
1807
 
1848
1808
        if revision.delta is not None and revision.delta.has_changed():
1849
1809
            for c in revision.delta.added + revision.delta.removed + revision.delta.modified:
1850
 
                if c.path[0] is None:
1851
 
                    path = c.path[1]
1852
 
                else:
1853
 
                    path = c.path[0]
 
1810
                path, = c[:1]
1854
1811
                to_file.write('\t* %s:\n' % (path,))
1855
 
            for c in revision.delta.renamed + revision.delta.copied:
 
1812
            for c in revision.delta.renamed:
 
1813
                oldpath, newpath = c[:2]
1856
1814
                # For renamed files, show both the old and the new path
1857
 
                to_file.write('\t* %s:\n\t* %s:\n' % (c.path[0], c.path[1]))
 
1815
                to_file.write('\t* %s:\n\t* %s:\n' % (oldpath, newpath))
1858
1816
            to_file.write('\n')
1859
1817
 
1860
1818
        if not revision.rev.message:
1913
1871
    try:
1914
1872
        return log_formatter_registry.make_formatter(name, *args, **kwargs)
1915
1873
    except KeyError:
1916
 
        raise errors.CommandError(
 
1874
        raise errors.BzrCommandError(
1917
1875
            gettext("unknown log formatter: %r") % name)
1918
1876
 
1919
1877
 
1991
1949
        to_file.write('Added Revisions:\n')
1992
1950
        show_log(branch,
1993
1951
                 lf,
 
1952
                 None,
1994
1953
                 verbose=False,
1995
1954
                 direction='forward',
1996
1955
                 start_revision=base_idx + 1,
1997
 
                 end_revision=len(new_rh))
 
1956
                 end_revision=len(new_rh),
 
1957
                 search=None)
1998
1958
 
1999
1959
 
2000
1960
def get_history_change(old_revision_id, new_revision_id, repository):
2074
2034
    if new_history != []:
2075
2035
        output.write('Added Revisions:\n')
2076
2036
        start_revno = new_revno - len(new_history) + 1
2077
 
        show_log(branch, lf, verbose=False, direction='forward',
 
2037
        show_log(branch, lf, None, verbose=False, direction='forward',
2078
2038
                 start_revision=start_revno)
2079
2039
 
2080
2040
 
2092
2052
        lf.log_revision(lr)
2093
2053
 
2094
2054
 
2095
 
def _get_info_for_log_files(revisionspec_list, file_list, exit_stack):
2096
 
    """Find files and kinds given a list of files and a revision range.
 
2055
def _get_info_for_log_files(revisionspec_list, file_list, add_cleanup):
 
2056
    """Find file-ids and kinds given a list of files and a revision range.
2097
2057
 
2098
2058
    We search for files at the end of the range. If not found there,
2099
2059
    we try the start of the range.
2102
2062
    :param file_list: the list of paths given on the command line;
2103
2063
      the first of these can be a branch location or a file path,
2104
2064
      the remainder must be file paths
2105
 
    :param exit_stack: When the branch returned is read locked,
2106
 
      an unlock call will be queued to the exit stack.
 
2065
    :param add_cleanup: When the branch returned is read locked,
 
2066
      an unlock call will be queued to the cleanup.
2107
2067
    :return: (branch, info_list, start_rev_info, end_rev_info) where
2108
 
      info_list is a list of (relative_path, found, kind) tuples where
 
2068
      info_list is a list of (relative_path, file_id, kind) tuples where
2109
2069
      kind is one of values 'directory', 'file', 'symlink', 'tree-reference'.
2110
2070
      branch will be read-locked.
2111
2071
    """
2112
2072
    from breezy.builtins import _get_revision_range
2113
2073
    tree, b, path = controldir.ControlDir.open_containing_tree_or_branch(
2114
2074
        file_list[0])
2115
 
    exit_stack.enter_context(b.lock_read())
 
2075
    add_cleanup(b.lock_read().unlock)
2116
2076
    # XXX: It's damn messy converting a list of paths to relative paths when
2117
2077
    # those paths might be deleted ones, they might be on a case-insensitive
2118
2078
    # filesystem and/or they might be in silly locations (like another branch).
2135
2095
            tree = b.basis_tree()
2136
2096
        tree1 = None
2137
2097
        for fp in relpaths:
2138
 
            kind = _get_kind_for_file(tree, fp)
2139
 
            if not kind:
 
2098
            file_id = tree.path2id(fp)
 
2099
            kind = _get_kind_for_file_id(tree, fp, file_id)
 
2100
            if file_id is None:
2140
2101
                # go back to when time began
2141
2102
                if tree1 is None:
2142
2103
                    try:
2143
2104
                        rev1 = b.get_rev_id(1)
2144
2105
                    except errors.NoSuchRevision:
2145
2106
                        # No history at all
 
2107
                        file_id = None
2146
2108
                        kind = None
2147
2109
                    else:
2148
2110
                        tree1 = b.repository.revision_tree(rev1)
2149
2111
                if tree1:
2150
 
                    kind = _get_kind_for_file(tree1, fp)
2151
 
            info_list.append((fp, kind))
 
2112
                    file_id = tree1.path2id(fp)
 
2113
                    kind = _get_kind_for_file_id(tree1, fp, file_id)
 
2114
            info_list.append((fp, file_id, kind))
2152
2115
 
2153
2116
    elif start_rev_info == end_rev_info:
2154
2117
        # One revision given - file must exist in it
2155
2118
        tree = b.repository.revision_tree(end_rev_info.rev_id)
2156
2119
        for fp in relpaths:
2157
 
            kind = _get_kind_for_file(tree, fp)
2158
 
            info_list.append((fp, kind))
 
2120
            file_id = tree.path2id(fp)
 
2121
            kind = _get_kind_for_file_id(tree, fp, file_id)
 
2122
            info_list.append((fp, file_id, kind))
2159
2123
 
2160
2124
    else:
2161
2125
        # Revision range given. Get the file-id from the end tree.
2167
2131
            tree = b.repository.revision_tree(rev_id)
2168
2132
        tree1 = None
2169
2133
        for fp in relpaths:
2170
 
            kind = _get_kind_for_file(tree, fp)
2171
 
            if not kind:
 
2134
            file_id = tree.path2id(fp)
 
2135
            kind = _get_kind_for_file_id(tree, fp, file_id)
 
2136
            if file_id is None:
2172
2137
                if tree1 is None:
2173
2138
                    rev_id = start_rev_info.rev_id
2174
2139
                    if rev_id is None:
2176
2141
                        tree1 = b.repository.revision_tree(rev1)
2177
2142
                    else:
2178
2143
                        tree1 = b.repository.revision_tree(rev_id)
2179
 
                kind = _get_kind_for_file(tree1, fp)
2180
 
            info_list.append((fp, kind))
 
2144
                file_id = tree1.path2id(fp)
 
2145
                kind = _get_kind_for_file_id(tree1, fp, file_id)
 
2146
            info_list.append((fp, file_id, kind))
2181
2147
    return b, info_list, start_rev_info, end_rev_info
2182
2148
 
2183
2149
 
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
 
2150
def _get_kind_for_file_id(tree, path, file_id):
 
2151
    """Return the kind of a file-id or None if it doesn't exist."""
 
2152
    if file_id is not None:
 
2153
        return tree.kind(path)
 
2154
    else:
 
2155
        return None
2191
2156
 
2192
2157
 
2193
2158
properties_handler_registry = registry.Registry()