/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: 2017-06-08 23:30:31 UTC
  • mto: This revision was merged to the branch mainline in revision 6690.
  • Revision ID: jelmer@jelmer.uk-20170608233031-3qavls2o7a1pqllj
Update imports.

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
63
64
    config,
64
65
    controldir,
65
66
    diff,
 
67
    errors,
66
68
    foreign,
67
 
    lazy_regex,
 
69
    repository as _mod_repository,
68
70
    revision as _mod_revision,
 
71
    revisionspec,
 
72
    tsort,
69
73
    )
70
74
from breezy.i18n import gettext, ngettext
71
75
""")
72
76
 
73
77
from . import (
74
 
    errors,
 
78
    lazy_regex,
75
79
    registry,
76
 
    revisionspec,
77
 
    trace,
78
80
    )
79
81
from .osutils import (
80
82
    format_date,
81
83
    format_date_with_offset_in_original_timezone,
82
84
    get_diff_header_encoding,
83
85
    get_terminal_encoding,
84
 
    is_inside,
85
86
    terminal_width,
86
87
    )
87
 
from .tree import (
88
 
    find_previous_path,
89
 
    InterTree,
 
88
from breezy.sixish import (
 
89
    BytesIO,
 
90
    range,
 
91
    zip,
90
92
    )
91
93
 
92
94
 
93
 
def find_touching_revisions(repository, last_revision, last_tree, last_path):
94
 
    """Yield a description of revisions which affect the file.
 
95
def find_touching_revisions(branch, file_id):
 
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
 
101
103
    TODO: Perhaps some way to limit this to only particular revisions,
102
104
    or to traverse a non-mainline set of revisions?
103
105
    """
104
 
    last_verifier = last_tree.get_file_verifier(last_path)
105
 
    graph = repository.get_graph()
106
 
    history = list(graph.iter_lefthand_ancestry(last_revision, []))
107
 
    revno = len(history)
108
 
    for revision_id in history:
109
 
        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)
 
106
    last_ie = None
 
107
    last_path = None
 
108
    revno = 1
 
109
    graph = branch.repository.get_graph()
 
110
    history = list(graph.iter_lefthand_ancestry(branch.last_revision(),
 
111
        [_mod_revision.NULL_REVISION]))
 
112
    for revision_id in reversed(history):
 
113
        this_inv = branch.repository.get_inventory(revision_id)
 
114
        if this_inv.has_id(file_id):
 
115
            this_ie = this_inv[file_id]
 
116
            this_path = this_inv.id2path(file_id)
 
117
        else:
 
118
            this_ie = this_path = None
112
119
 
113
120
        # now we know how it was last time, and how it is in this revision.
114
121
        # are those two states effectively the same or not?
115
 
        if this_path is not None and last_path is None:
116
 
            yield revno, revision_id, "deleted " + this_path
117
 
            this_verifier = this_tree.get_file_verifier(this_path)
118
 
        elif this_path is None and last_path is not None:
119
 
            yield revno, revision_id, "added " + last_path
 
122
 
 
123
        if not this_ie and not last_ie:
 
124
            # not present in either
 
125
            pass
 
126
        elif this_ie and not last_ie:
 
127
            yield revno, revision_id, "added " + this_path
 
128
        elif not this_ie and last_ie:
 
129
            # deleted here
 
130
            yield revno, revision_id, "deleted " + last_path
120
131
        elif this_path != last_path:
121
 
            yield revno, revision_id, ("renamed %s => %s" % (this_path, last_path))
122
 
            this_verifier = this_tree.get_file_verifier(this_path)
123
 
        else:
124
 
            this_verifier = this_tree.get_file_verifier(this_path)
125
 
            if (this_verifier != last_verifier):
126
 
                yield revno, revision_id, "modified " + this_path
 
132
            yield revno, revision_id, ("renamed %s => %s" % (last_path, this_path))
 
133
        elif (this_ie.text_size != last_ie.text_size
 
134
              or this_ie.text_sha1 != last_ie.text_sha1):
 
135
            yield revno, revision_id, "modified " + this_path
127
136
 
128
 
        last_verifier = this_verifier
 
137
        last_ie = this_ie
129
138
        last_path = this_path
130
 
        last_tree = this_tree
131
 
        if last_path is None:
132
 
            return
133
 
        revno -= 1
 
139
        revno += 1
134
140
 
135
141
 
136
142
def show_log(branch,
137
143
             lf,
 
144
             specific_fileid=None,
138
145
             verbose=False,
139
146
             direction='reverse',
140
147
             start_revision=None,
141
148
             end_revision=None,
 
149
             search=None,
142
150
             limit=None,
143
151
             show_diff=False,
144
152
             match=None):
151
159
 
152
160
    :param lf: The LogFormatter object showing the output.
153
161
 
 
162
    :param specific_fileid: If not None, list only the commits affecting the
 
163
        specified file, rather than all commits.
 
164
 
154
165
    :param verbose: If True show added/changed/deleted/renamed files.
155
166
 
156
167
    :param direction: 'reverse' (default) is latest to earliest; 'forward' is
160
171
 
161
172
    :param end_revision: If not None, only show revisions <= end_revision
162
173
 
 
174
    :param search: If not None, only show revisions with matching commit
 
175
        messages
 
176
 
163
177
    :param limit: If set, shows only 'limit' revisions, all revisions are shown
164
178
        if None or 0.
165
179
 
168
182
    :param match: Dictionary of search lists to use when matching revision
169
183
      properties.
170
184
    """
 
185
    # Convert old-style parameters to new-style parameters
 
186
    if specific_fileid is not None:
 
187
        file_ids = [specific_fileid]
 
188
    else:
 
189
        file_ids = None
171
190
    if verbose:
172
 
        delta_type = 'full'
 
191
        if file_ids:
 
192
            delta_type = 'partial'
 
193
        else:
 
194
            delta_type = 'full'
173
195
    else:
174
196
        delta_type = None
175
197
    if show_diff:
176
 
        diff_type = 'full'
 
198
        if file_ids:
 
199
            diff_type = 'partial'
 
200
        else:
 
201
            diff_type = 'full'
177
202
    else:
178
203
        diff_type = None
179
204
 
180
 
    if isinstance(start_revision, int):
181
 
        try:
182
 
            start_revision = revisionspec.RevisionInfo(branch, start_revision)
183
 
        except (errors.NoSuchRevision, errors.RevnoOutOfBounds):
184
 
            raise errors.InvalidRevisionNumber(start_revision)
185
 
 
186
 
    if isinstance(end_revision, int):
187
 
        try:
188
 
            end_revision = revisionspec.RevisionInfo(branch, end_revision)
189
 
        except (errors.NoSuchRevision, errors.RevnoOutOfBounds):
190
 
            raise errors.InvalidRevisionNumber(end_revision)
191
 
 
192
 
    if end_revision is not None and end_revision.revno == 0:
193
 
        raise errors.InvalidRevisionNumber(end_revision.revno)
194
 
 
195
205
    # Build the request and execute it
196
 
    rqst = make_log_request_dict(
197
 
        direction=direction,
 
206
    rqst = make_log_request_dict(direction=direction, specific_fileids=file_ids,
198
207
        start_revision=start_revision, end_revision=end_revision,
199
 
        limit=limit, delta_type=delta_type, diff_type=diff_type)
 
208
        limit=limit, message_search=search,
 
209
        delta_type=delta_type, diff_type=diff_type)
200
210
    Logger(branch, rqst).show(lf)
201
211
 
202
212
 
211
221
    }
212
222
 
213
223
 
214
 
def make_log_request_dict(direction='reverse', specific_files=None,
 
224
def make_log_request_dict(direction='reverse', specific_fileids=None,
215
225
                          start_revision=None, end_revision=None, limit=None,
216
226
                          message_search=None, levels=None, generate_tags=True,
217
227
                          delta_type=None,
228
238
    :param direction: 'reverse' (default) is latest to earliest;
229
239
      'forward' is earliest to latest.
230
240
 
231
 
    :param specific_files: If not None, only include revisions
 
241
    :param specific_fileids: If not None, only include revisions
232
242
      affecting the specified files, rather than all revisions.
233
243
 
234
244
    :param start_revision: If not None, only generate
251
261
`
252
262
    :param delta_type: Either 'full', 'partial' or None.
253
263
      'full' means generate the complete delta - adds/deletes/modifies/etc;
254
 
      'partial' means filter the delta using specific_files;
 
264
      'partial' means filter the delta using specific_fileids;
255
265
      None means do not generate any delta.
256
266
 
257
267
    :param diff_type: Either 'full', 'partial' or None.
258
268
      'full' means generate the complete diff - adds/deletes/modifies/etc;
259
 
      'partial' means filter the diff using specific_files;
 
269
      'partial' means filter the diff using specific_fileids;
260
270
      None means do not generate any diff.
261
271
 
262
272
    :param _match_using_deltas: a private parameter controlling the
263
 
      algorithm used for matching specific_files. This parameter
 
273
      algorithm used for matching specific_fileids. This parameter
264
274
      may be removed in the future so breezy client code should NOT
265
275
      use it.
266
276
 
285
295
            else:
286
296
                match['message'] = [message_search]
287
297
        else:
288
 
            match = {'message': [message_search]}
 
298
            match={ 'message': [message_search] }
289
299
    return {
290
300
        'direction': direction,
291
 
        'specific_files': specific_files,
 
301
        'specific_fileids': specific_fileids,
292
302
        'start_revision': start_revision,
293
303
        'end_revision': end_revision,
294
304
        'limit': limit,
313
323
    return result
314
324
 
315
325
 
316
 
def format_signature_validity(rev_id, branch):
 
326
def format_signature_validity(rev_id, repo):
317
327
    """get the signature validity
318
328
 
319
329
    :param rev_id: revision id to validate
320
 
    :param branch: branch of revision
 
330
    :param repo: repository of revision
321
331
    :return: human readable string to print to log
322
332
    """
323
333
    from breezy import gpg
324
334
 
325
 
    gpg_strategy = gpg.GPGStrategy(branch.get_config_stack())
326
 
    result = branch.repository.verify_revision_signature(rev_id, gpg_strategy)
 
335
    gpg_strategy = gpg.GPGStrategy(None)
 
336
    result = repo.verify_revision_signature(rev_id, gpg_strategy)
327
337
    if result[0] == gpg.SIGNATURE_VALID:
328
338
        return u"valid signature from {0}".format(result[1])
329
339
    if result[0] == gpg.SIGNATURE_KEY_MISSING:
366
376
        if not isinstance(lf, LogFormatter):
367
377
            warn("not a LogFormatter instance: %r" % lf)
368
378
 
369
 
        with self.branch.lock_read():
 
379
        self.branch.lock_read()
 
380
        try:
370
381
            if getattr(lf, 'begin_log', None):
371
382
                lf.begin_log()
372
383
            self._show_body(lf)
373
384
            if getattr(lf, 'end_log', None):
374
385
                lf.end_log()
 
386
        finally:
 
387
            self.branch.unlock()
375
388
 
376
389
    def _show_body(self, lf):
377
390
        """Show the main log output.
396
409
 
397
410
        # Find and print the interesting revisions
398
411
        generator = self._generator_factory(self.branch, rqst)
399
 
        try:
400
 
            for lr in generator.iter_log_revisions():
401
 
                lf.log_revision(lr)
402
 
        except errors.GhostRevisionUnusableHere:
403
 
            raise errors.CommandError(
404
 
                gettext('Further revision history missing.'))
 
412
        for lr in generator.iter_log_revisions():
 
413
            lf.log_revision(lr)
405
414
        lf.show_advice()
406
415
 
407
416
    def _generator_factory(self, branch, rqst):
409
418
 
410
419
        Subclasses may wish to override this.
411
420
        """
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()
 
421
        return _DefaultLogGenerator(branch, rqst)
479
422
 
480
423
 
481
424
class _StartNotLinearAncestor(Exception):
485
428
class _DefaultLogGenerator(LogGenerator):
486
429
    """The default generator of log revisions."""
487
430
 
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):
 
431
    def __init__(self, branch, rqst):
495
432
        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():
 
433
        self.rqst = rqst
 
434
        if rqst.get('generate_tags') and branch.supports_tags():
510
435
            self.rev_tag_dict = branch.tags.get_reverse_tag_dict()
511
436
        else:
512
437
            self.rev_tag_dict = {}
516
441
 
517
442
        :return: An iterator yielding LogRevision objects.
518
443
        """
 
444
        rqst = self.rqst
 
445
        levels = rqst.get('levels')
 
446
        limit = rqst.get('limit')
 
447
        diff_type = rqst.get('diff_type')
 
448
        show_signature = rqst.get('signature')
 
449
        omit_merges = rqst.get('omit_merges')
519
450
        log_count = 0
520
451
        revision_iterator = self._create_log_revision_iterator()
521
452
        for revs in revision_iterator:
522
453
            for (rev_id, revno, merge_depth), rev, delta in revs:
523
454
                # 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):
526
 
                    continue
527
 
                if self.omit_merges and len(rev.parent_ids) > 1:
528
 
                    continue
529
 
                if rev is None:
530
 
                    raise errors.GhostRevisionUnusableHere(rev_id)
531
 
                if self.diff_type is None:
 
455
                if levels != 0 and merge_depth >= levels:
 
456
                    continue
 
457
                if omit_merges and len(rev.parent_ids) > 1:
 
458
                    continue
 
459
                if diff_type is None:
532
460
                    diff = None
533
461
                else:
534
 
                    diff = _format_diff(
535
 
                        self.branch, rev, self.diff_type,
536
 
                        self.specific_files)
537
 
                if self.show_signature:
538
 
                    signature = format_signature_validity(rev_id, self.branch)
 
462
                    diff = self._format_diff(rev, rev_id, diff_type)
 
463
                if show_signature:
 
464
                    signature = format_signature_validity(rev_id,
 
465
                                                self.branch.repository)
539
466
                else:
540
467
                    signature = None
541
 
                yield LogRevision(
542
 
                    rev, revno, merge_depth, delta,
 
468
                yield LogRevision(rev, revno, merge_depth, delta,
543
469
                    self.rev_tag_dict.get(rev_id), diff, signature)
544
 
                if self.limit:
 
470
                if limit:
545
471
                    log_count += 1
546
 
                    if log_count >= self.limit:
 
472
                    if log_count >= limit:
547
473
                        return
548
474
 
 
475
    def _format_diff(self, rev, rev_id, diff_type):
 
476
        repo = self.branch.repository
 
477
        if len(rev.parent_ids) == 0:
 
478
            ancestor_id = _mod_revision.NULL_REVISION
 
479
        else:
 
480
            ancestor_id = rev.parent_ids[0]
 
481
        tree_1 = repo.revision_tree(ancestor_id)
 
482
        tree_2 = repo.revision_tree(rev_id)
 
483
        file_ids = self.rqst.get('specific_fileids')
 
484
        if diff_type == 'partial' and file_ids is not None:
 
485
            specific_files = [tree_2.id2path(id) for id in file_ids]
 
486
        else:
 
487
            specific_files = None
 
488
        s = BytesIO()
 
489
        path_encoding = get_diff_header_encoding()
 
490
        diff.show_diff_trees(tree_1, tree_2, s, specific_files, old_label='',
 
491
            new_label='', path_encoding=path_encoding)
 
492
        return s.getvalue()
 
493
 
549
494
    def _create_log_revision_iterator(self):
550
495
        """Create a revision iterator for log.
551
496
 
552
497
        :return: An iterator over lists of ((rev_id, revno, merge_depth), rev,
553
498
            delta).
554
499
        """
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)
 
500
        self.start_rev_id, self.end_rev_id = _get_revision_limits(
 
501
            self.branch, self.rqst.get('start_revision'),
 
502
            self.rqst.get('end_revision'))
 
503
        if self.rqst.get('_match_using_deltas'):
 
504
            return self._log_revision_iterator_using_delta_matching()
568
505
        else:
569
506
            # We're using the per-file-graph algorithm. This scales really
570
507
            # well but only makes sense if there is a single file and it's
571
508
            # not a directory
572
 
            file_count = len(self.specific_files)
 
509
            file_count = len(self.rqst.get('specific_fileids'))
573
510
            if file_count != 1:
574
 
                raise errors.BzrError(
575
 
                    "illegal LogRequest: must match-using-deltas "
 
511
                raise BzrError("illegal LogRequest: must match-using-deltas "
576
512
                    "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
 
                )
 
513
            return self._log_revision_iterator_using_per_file_graph()
 
514
 
 
515
    def _log_revision_iterator_using_delta_matching(self):
 
516
        # Get the base revisions, filtering by the revision range
 
517
        rqst = self.rqst
 
518
        generate_merge_revisions = rqst.get('levels') != 1
 
519
        delayed_graph_generation = not rqst.get('specific_fileids') and (
 
520
                rqst.get('limit') or self.start_rev_id or self.end_rev_id)
 
521
        view_revisions = _calc_view_revisions(
 
522
            self.branch, self.start_rev_id, self.end_rev_id,
 
523
            rqst.get('direction'),
 
524
            generate_merge_revisions=generate_merge_revisions,
 
525
            delayed_graph_generation=delayed_graph_generation,
 
526
            exclude_common_ancestry=rqst.get('exclude_common_ancestry'))
 
527
 
 
528
        # Apply the other filters
 
529
        return make_log_rev_iterator(self.branch, view_revisions,
 
530
            rqst.get('delta_type'), rqst.get('match'),
 
531
            file_ids=rqst.get('specific_fileids'),
 
532
            direction=rqst.get('direction'))
 
533
 
 
534
    def _log_revision_iterator_using_per_file_graph(self):
 
535
        # Get the base revisions, filtering by the revision range.
 
536
        # Note that we always generate the merge revisions because
 
537
        # filter_revisions_touching_file_id() requires them ...
 
538
        rqst = self.rqst
 
539
        view_revisions = _calc_view_revisions(
 
540
            self.branch, self.start_rev_id, self.end_rev_id,
 
541
            rqst.get('direction'), generate_merge_revisions=True,
 
542
            exclude_common_ancestry=rqst.get('exclude_common_ancestry'))
 
543
        if not isinstance(view_revisions, list):
 
544
            view_revisions = list(view_revisions)
 
545
        view_revisions = _filter_revisions_touching_file_id(self.branch,
 
546
            rqst.get('specific_fileids')[0], view_revisions,
 
547
            include_merges=rqst.get('levels') != 1)
 
548
        return make_log_rev_iterator(self.branch, view_revisions,
 
549
            rqst.get('delta_type'), rqst.get('match'))
587
550
 
588
551
 
589
552
def _calc_view_revisions(branch, start_rev_id, end_rev_id, direction,
597
560
             a list of the same tuples.
598
561
    """
599
562
    if (exclude_common_ancestry and start_rev_id == end_rev_id):
600
 
        raise errors.CommandError(gettext(
 
563
        raise errors.BzrCommandError(gettext(
601
564
            '--exclude-common-ancestry requires two different revisions'))
602
565
    if direction not in ('reverse', 'forward'):
603
566
        raise ValueError(gettext('invalid direction %r') % direction)
604
 
    br_rev_id = branch.last_revision()
605
 
    if br_rev_id == _mod_revision.NULL_REVISION:
 
567
    br_revno, br_rev_id = branch.last_revision_info()
 
568
    if br_revno == 0:
606
569
        return []
607
570
 
608
571
    if (end_rev_id and start_rev_id == end_rev_id
609
572
        and (not generate_merge_revisions
610
573
             or not _has_merges(branch, end_rev_id))):
611
574
        # If a single revision is requested, check we can handle it
612
 
        return _generate_one_revision(branch, end_rev_id, br_rev_id,
613
 
                                      branch.revno())
 
575
        return  _generate_one_revision(branch, end_rev_id, br_rev_id,
 
576
                                       br_revno)
614
577
    if not generate_merge_revisions:
615
578
        try:
616
579
            # If we only want to see linear revisions, we can iterate ...
621
584
            # ancestor of the end limit, check it before outputting anything
622
585
            if (direction == 'forward'
623
586
                or (start_rev_id and not _is_obvious_ancestor(
624
 
                    branch, start_rev_id, end_rev_id))):
625
 
                iter_revs = list(iter_revs)
 
587
                        branch, start_rev_id, end_rev_id))):
 
588
                    iter_revs = list(iter_revs)
626
589
            if direction == 'forward':
627
590
                iter_revs = reversed(iter_revs)
628
591
            return iter_revs
660
623
    initial_revisions = []
661
624
    if delayed_graph_generation:
662
625
        try:
663
 
            for rev_id, revno, depth in _linear_view_revisions(
664
 
                    branch, start_rev_id, end_rev_id, exclude_common_ancestry):
 
626
            for rev_id, revno, depth in  _linear_view_revisions(
 
627
                branch, start_rev_id, end_rev_id, exclude_common_ancestry):
665
628
                if _has_merges(branch, rev_id):
666
629
                    # The end_rev_id can be nested down somewhere. We need an
667
630
                    # explicit ancestry check. There is an ambiguity here as we
674
637
                    # -- vila 20100319
675
638
                    graph = branch.repository.get_graph()
676
639
                    if (start_rev_id is not None
677
 
                            and not graph.is_ancestor(start_rev_id, end_rev_id)):
 
640
                        and not graph.is_ancestor(start_rev_id, end_rev_id)):
678
641
                        raise _StartNotLinearAncestor()
679
642
                    # Since we collected the revisions so far, we need to
680
643
                    # adjust end_rev_id.
688
651
        except _StartNotLinearAncestor:
689
652
            # A merge was never detected so the lower revision limit can't
690
653
            # be nested down somewhere
691
 
            raise errors.CommandError(gettext('Start revision not found in'
692
 
                                                 ' history of end revision.'))
 
654
            raise errors.BzrCommandError(gettext('Start revision not found in'
 
655
                ' history of end revision.'))
693
656
 
694
657
    # We exit the loop above because we encounter a revision with merges, from
695
658
    # this revision, we need to switch to _graph_view_revisions.
700
663
    # make forward the exact opposite display, but showing the merge revisions
701
664
    # indented at the end seems slightly nicer in that case.
702
665
    view_revisions = itertools.chain(iter(initial_revisions),
703
 
                                     _graph_view_revisions(branch, start_rev_id, end_rev_id,
704
 
                                                           rebase_initial_depths=(
705
 
                                                               direction == 'reverse'),
706
 
                                                           exclude_common_ancestry=exclude_common_ancestry))
 
666
        _graph_view_revisions(branch, start_rev_id, end_rev_id,
 
667
                              rebase_initial_depths=(direction == 'reverse'),
 
668
                              exclude_common_ancestry=exclude_common_ancestry))
707
669
    return view_revisions
708
670
 
709
671
 
741
703
            # both on mainline
742
704
            return start_dotted[0] <= end_dotted[0]
743
705
        elif (len(start_dotted) == 3 and len(end_dotted) == 3 and
744
 
              start_dotted[0:1] == end_dotted[0:1]):
 
706
            start_dotted[0:1] == end_dotted[0:1]):
745
707
            # both on same development line
746
708
            return start_dotted[2] <= end_dotted[2]
747
709
        else:
761
723
    :param exclude_common_ancestry: Whether the start_rev_id should be part of
762
724
        the iterated revisions.
763
725
    :return: An iterator of (revision_id, dotted_revno, merge_depth) tuples.
764
 
        dotted_revno will be None for ghosts
765
726
    :raises _StartNotLinearAncestor: if a start_rev_id is specified but
766
727
        is not found walking the left-hand history
767
728
    """
 
729
    br_revno, br_rev_id = branch.last_revision_info()
768
730
    repo = branch.repository
769
731
    graph = repo.get_graph()
770
732
    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:
781
 
            br_rev_id = branch.last_revision()
782
 
            cur_revno = None
783
 
 
784
 
        graph_iter = graph.iter_lefthand_ancestry(br_rev_id,
785
 
                                                  (_mod_revision.NULL_REVISION,))
786
 
        while True:
787
 
            try:
788
 
                revision_id = next(graph_iter)
789
 
            except errors.RevisionNotPresent as e:
790
 
                # Oops, a ghost.
791
 
                yield e.revision_id, None, None
792
 
                break
793
 
            except StopIteration:
794
 
                break
795
 
            else:
796
 
                yield revision_id, str(cur_revno) if cur_revno is not None else None, 0
797
 
                if cur_revno is not None:
798
 
                    cur_revno -= 1
 
733
        cur_revno = br_revno
 
734
        for revision_id in graph.iter_lefthand_ancestry(br_rev_id,
 
735
            (_mod_revision.NULL_REVISION,)):
 
736
            yield revision_id, str(cur_revno), 0
 
737
            cur_revno -= 1
799
738
    else:
800
 
        br_rev_id = branch.last_revision()
801
739
        if end_rev_id is None:
802
740
            end_rev_id = br_rev_id
803
741
        found_start = start_rev_id is None
804
 
        graph_iter = graph.iter_lefthand_ancestry(end_rev_id,
805
 
                                                  (_mod_revision.NULL_REVISION,))
806
 
        while True:
807
 
            try:
808
 
                revision_id = next(graph_iter)
809
 
            except StopIteration:
810
 
                break
811
 
            except errors.RevisionNotPresent as e:
812
 
                # Oops, a ghost.
813
 
                yield e.revision_id, None, None
814
 
                break
815
 
            else:
816
 
                revno_str = _compute_revno_str(branch, revision_id)
817
 
                if not found_start and revision_id == start_rev_id:
818
 
                    if not exclude_common_ancestry:
819
 
                        yield revision_id, revno_str, 0
820
 
                    found_start = True
821
 
                    break
822
 
                else:
 
742
        for revision_id in graph.iter_lefthand_ancestry(end_rev_id,
 
743
                (_mod_revision.NULL_REVISION,)):
 
744
            revno_str = _compute_revno_str(branch, revision_id)
 
745
            if not found_start and revision_id == start_rev_id:
 
746
                if not exclude_common_ancestry:
823
747
                    yield revision_id, revno_str, 0
824
 
        if not found_start:
825
 
            raise _StartNotLinearAncestor()
 
748
                found_start = True
 
749
                break
 
750
            else:
 
751
                yield revision_id, revno_str, 0
 
752
        else:
 
753
            if not found_start:
 
754
                raise _StartNotLinearAncestor()
826
755
 
827
756
 
828
757
def _graph_view_revisions(branch, start_rev_id, end_rev_id,
873
802
    """Adjust depths upwards so the top level is 0."""
874
803
    # If either the first or last revision have a merge_depth of 0, we're done
875
804
    if view_revisions and view_revisions[0][2] and view_revisions[-1][2]:
876
 
        min_depth = min([d for r, n, d in view_revisions])
 
805
        min_depth = min([d for r,n,d in view_revisions])
877
806
        if min_depth != 0:
878
 
            view_revisions = [(r, n, d - min_depth)
879
 
                              for r, n, d in view_revisions]
 
807
            view_revisions = [(r,n,d-min_depth) for r,n,d in view_revisions]
880
808
    return view_revisions
881
809
 
882
810
 
883
811
def make_log_rev_iterator(branch, view_revisions, generate_delta, search,
884
 
                          files=None, direction='reverse'):
 
812
        file_ids=None, direction='reverse'):
885
813
    """Create a revision iterator for log.
886
814
 
887
815
    :param branch: The branch being logged.
889
817
    :param generate_delta: Whether to generate a delta for each revision.
890
818
      Permitted values are None, 'full' and 'partial'.
891
819
    :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.
 
820
    :param file_ids: If non empty, only revisions matching one or more of
 
821
      the file-ids are to be kept.
894
822
    :param direction: the direction in which view_revisions is sorted
895
823
    :return: An iterator over lists of ((rev_id, revno, merge_depth), rev,
896
824
        delta).
911
839
        # It would be nicer if log adapters were first class objects
912
840
        # with custom parameters. This will do for now. IGC 20090127
913
841
        if adapter == _make_delta_filter:
914
 
            log_rev_iterator = adapter(
915
 
                branch, generate_delta, search, log_rev_iterator, files,
916
 
                direction)
 
842
            log_rev_iterator = adapter(branch, generate_delta,
 
843
                search, log_rev_iterator, file_ids, direction)
917
844
        else:
918
 
            log_rev_iterator = adapter(
919
 
                branch, generate_delta, search, log_rev_iterator)
 
845
            log_rev_iterator = adapter(branch, generate_delta,
 
846
                search, log_rev_iterator)
920
847
    return log_rev_iterator
921
848
 
922
849
 
934
861
    :return: An iterator over lists of ((rev_id, revno, merge_depth), rev,
935
862
        delta).
936
863
    """
937
 
    if not match:
 
864
    if match is None:
938
865
        return log_rev_iterator
939
 
    # Use lazy_compile so mapping to InvalidPattern error occurs.
940
 
    searchRE = [(k, [lazy_regex.lazy_compile(x, re.IGNORECASE) for x in v])
 
866
    searchRE = [(k, [re.compile(x, re.IGNORECASE) for x in v])
941
867
                for k, v in match.items()]
942
868
    return _filter_re(searchRE, log_rev_iterator)
943
869
 
948
874
        if new_revs:
949
875
            yield new_revs
950
876
 
951
 
 
952
877
def _match_filter(searchRE, rev):
953
878
    strings = {
954
 
        'message': (rev.message,),
955
 
        'committer': (rev.committer,),
956
 
        'author': (rev.get_apparent_authors()),
957
 
        'bugs': list(rev.iter_bugs())
958
 
        }
 
879
               'message': (rev.message,),
 
880
               'committer': (rev.committer,),
 
881
               'author': (rev.get_apparent_authors()),
 
882
               'bugs': list(rev.iter_bugs())
 
883
               }
959
884
    strings[''] = [item for inner_list in strings.values()
960
885
                   for item in inner_list]
961
 
    for k, v in searchRE:
 
886
    for (k,v) in searchRE:
962
887
        if k in strings and not _match_any_filter(strings[k], v):
963
888
            return False
964
889
    return True
965
890
 
966
 
 
967
891
def _match_any_filter(strings, res):
968
 
    return any(r.search(s) for r in res for s in strings)
969
 
 
 
892
    return any(re.search(s) for re in res for s in strings)
970
893
 
971
894
def _make_delta_filter(branch, generate_delta, search, log_rev_iterator,
972
 
                       files=None, direction='reverse'):
 
895
    fileids=None, direction='reverse'):
973
896
    """Add revision deltas to a log iterator if needed.
974
897
 
975
898
    :param branch: The branch being logged.
978
901
    :param search: A user text search string.
979
902
    :param log_rev_iterator: An input iterator containing all revisions that
980
903
        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.
 
904
    :param fileids: If non empty, only revisions matching one or more of
 
905
      the file-ids are to be kept.
983
906
    :param direction: the direction in which view_revisions is sorted
984
907
    :return: An iterator over lists of ((rev_id, revno, merge_depth), rev,
985
908
        delta).
986
909
    """
987
 
    if not generate_delta and not files:
 
910
    if not generate_delta and not fileids:
988
911
        return log_rev_iterator
989
912
    return _generate_deltas(branch.repository, log_rev_iterator,
990
 
                            generate_delta, files, direction)
991
 
 
992
 
 
993
 
def _generate_deltas(repository, log_rev_iterator, delta_type, files,
994
 
                     direction):
 
913
        generate_delta, fileids, direction)
 
914
 
 
915
 
 
916
def _generate_deltas(repository, log_rev_iterator, delta_type, fileids,
 
917
    direction):
995
918
    """Create deltas for each batch of revisions in log_rev_iterator.
996
919
 
997
920
    If we're only generating deltas for the sake of filtering against
998
 
    files, we stop generating deltas once all files reach the
 
921
    file-ids, we stop generating deltas once all file-ids reach the
999
922
    appropriate life-cycle point. If we're receiving data newest to
1000
923
    oldest, then that life-cycle point is 'add', otherwise it's 'remove'.
1001
924
    """
1002
 
    check_files = files is not None and len(files) > 0
1003
 
    if check_files:
1004
 
        file_set = set(files)
 
925
    check_fileids = fileids is not None and len(fileids) > 0
 
926
    if check_fileids:
 
927
        fileid_set = set(fileids)
1005
928
        if direction == 'reverse':
1006
929
            stop_on = 'add'
1007
930
        else:
1008
931
            stop_on = 'remove'
1009
932
    else:
1010
 
        file_set = None
 
933
        fileid_set = None
1011
934
    for revs in log_rev_iterator:
1012
 
        # If we were matching against files and we've run out,
 
935
        # If we were matching against fileids and we've run out,
1013
936
        # there's nothing left to do
1014
 
        if check_files and not file_set:
 
937
        if check_fileids and not fileid_set:
1015
938
            return
1016
939
        revisions = [rev[1] for rev in revs]
1017
940
        new_revs = []
1018
 
        if delta_type == 'full' and not check_files:
1019
 
            deltas = repository.get_revision_deltas(revisions)
 
941
        if delta_type == 'full' and not check_fileids:
 
942
            deltas = repository.get_deltas_for_revisions(revisions)
1020
943
            for rev, delta in zip(revs, deltas):
1021
944
                new_revs.append((rev[0], rev[1], delta))
1022
945
        else:
1023
 
            deltas = repository.get_revision_deltas(
1024
 
                revisions, specific_files=file_set)
 
946
            deltas = repository.get_deltas_for_revisions(revisions, fileid_set)
1025
947
            for rev, delta in zip(revs, deltas):
1026
 
                if check_files:
 
948
                if check_fileids:
1027
949
                    if delta is None or not delta.has_changed():
1028
950
                        continue
1029
951
                    else:
1030
 
                        _update_files(delta, file_set, stop_on)
 
952
                        _update_fileids(delta, fileid_set, stop_on)
1031
953
                        if delta_type is None:
1032
954
                            delta = None
1033
955
                        elif delta_type == 'full':
1044
966
        yield new_revs
1045
967
 
1046
968
 
1047
 
def _update_files(delta, files, stop_on):
1048
 
    """Update the set of files to search based on file lifecycle events.
 
969
def _update_fileids(delta, fileids, stop_on):
 
970
    """Update the set of file-ids to search based on file lifecycle events.
1049
971
 
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
 
972
    :param fileids: a set of fileids to update
 
973
    :param stop_on: either 'add' or 'remove' - take file-ids out of the
 
974
      fileids set once their add or remove entry is detected respectively
1053
975
    """
1054
976
    if stop_on == 'add':
1055
977
        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]):])
 
978
            if item[1] in fileids:
 
979
                fileids.remove(item[1])
1067
980
    elif stop_on == 'delete':
1068
981
        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]):])
 
982
            if item[1] in fileids:
 
983
                fileids.remove(item[1])
1080
984
 
1081
985
 
1082
986
def _make_revision_objects(branch, generate_delta, search, log_rev_iterator):
1094
998
    for revs in log_rev_iterator:
1095
999
        # r = revision_id, n = revno, d = merge depth
1096
1000
        revision_ids = [view[0] for view, _, _ in revs]
1097
 
        revisions = dict(repository.iter_revisions(revision_ids))
1098
 
        yield [(rev[0], revisions[rev[0][0]], rev[2]) for rev in revs]
 
1001
        revisions = repository.get_revisions(revision_ids)
 
1002
        revs = [(rev[0], revision, rev[2]) for rev, revision in
 
1003
            zip(revs, revisions)]
 
1004
        yield revs
1099
1005
 
1100
1006
 
1101
1007
def _make_batch_filter(branch, generate_delta, search, log_rev_iterator):
1123
1029
def _get_revision_limits(branch, start_revision, end_revision):
1124
1030
    """Get and check revision limits.
1125
1031
 
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
 
1032
    :param  branch: The branch containing the revisions.
 
1033
 
 
1034
    :param  start_revision: The first revision to be logged.
 
1035
            For backwards compatibility this may be a mainline integer revno,
 
1036
            but for merge revision support a RevisionInfo is expected.
 
1037
 
 
1038
    :param  end_revision: The last revision to be logged.
 
1039
            For backwards compatibility this may be a mainline integer revno,
 
1040
            but for merge revision support a RevisionInfo is expected.
1131
1041
 
1132
1042
    :return: (start_rev_id, end_rev_id) tuple.
1133
1043
    """
 
1044
    branch_revno, branch_rev_id = branch.last_revision_info()
1134
1045
    start_rev_id = None
1135
 
    start_revno = None
1136
 
    if start_revision is not None:
1137
 
        if not isinstance(start_revision, revisionspec.RevisionInfo):
1138
 
            raise TypeError(start_revision)
1139
 
        start_rev_id = start_revision.rev_id
1140
 
        start_revno = start_revision.revno
1141
 
    if start_revno is None:
 
1046
    if start_revision is None:
1142
1047
        start_revno = 1
 
1048
    else:
 
1049
        if isinstance(start_revision, revisionspec.RevisionInfo):
 
1050
            start_rev_id = start_revision.rev_id
 
1051
            start_revno = start_revision.revno or 1
 
1052
        else:
 
1053
            branch.check_real_revno(start_revision)
 
1054
            start_revno = start_revision
 
1055
            start_rev_id = branch.get_rev_id(start_revno)
1143
1056
 
1144
1057
    end_rev_id = None
1145
 
    end_revno = None
1146
 
    if end_revision is not None:
1147
 
        if not isinstance(end_revision, revisionspec.RevisionInfo):
1148
 
            raise TypeError(start_revision)
1149
 
        end_rev_id = end_revision.rev_id
1150
 
        end_revno = end_revision.revno
 
1058
    if end_revision is None:
 
1059
        end_revno = branch_revno
 
1060
    else:
 
1061
        if isinstance(end_revision, revisionspec.RevisionInfo):
 
1062
            end_rev_id = end_revision.rev_id
 
1063
            end_revno = end_revision.revno or branch_revno
 
1064
        else:
 
1065
            branch.check_real_revno(end_revision)
 
1066
            end_revno = end_revision
 
1067
            end_rev_id = branch.get_rev_id(end_revno)
1151
1068
 
1152
 
    if branch.last_revision() != _mod_revision.NULL_REVISION:
 
1069
    if branch_revno != 0:
1153
1070
        if (start_rev_id == _mod_revision.NULL_REVISION
1154
 
                or end_rev_id == _mod_revision.NULL_REVISION):
1155
 
            raise errors.CommandError(
1156
 
                gettext('Logging revision 0 is invalid.'))
1157
 
        if end_revno is not None and start_revno > end_revno:
1158
 
            raise errors.CommandError(
1159
 
                gettext("Start revision must be older than the end revision."))
 
1071
            or end_rev_id == _mod_revision.NULL_REVISION):
 
1072
            raise errors.BzrCommandError(gettext('Logging revision 0 is invalid.'))
 
1073
        if start_revno > end_revno:
 
1074
            raise errors.BzrCommandError(gettext("Start revision must be "
 
1075
                                         "older than the end revision."))
1160
1076
    return (start_rev_id, end_rev_id)
1161
1077
 
1162
1078
 
1210
1126
            end_revno = end_revision
1211
1127
 
1212
1128
    if ((start_rev_id == _mod_revision.NULL_REVISION)
1213
 
            or (end_rev_id == _mod_revision.NULL_REVISION)):
1214
 
        raise errors.CommandError(gettext('Logging revision 0 is invalid.'))
 
1129
        or (end_rev_id == _mod_revision.NULL_REVISION)):
 
1130
        raise errors.BzrCommandError(gettext('Logging revision 0 is invalid.'))
1215
1131
    if start_revno > end_revno:
1216
 
        raise errors.CommandError(gettext("Start revision must be older "
1217
 
                                             "than the end revision."))
 
1132
        raise errors.BzrCommandError(gettext("Start revision must be older "
 
1133
                                     "than the end revision."))
1218
1134
 
1219
1135
    if end_revno < start_revno:
1220
1136
        return None, None, None, None
1243
1159
    return mainline_revs, rev_nos, start_rev_id, end_rev_id
1244
1160
 
1245
1161
 
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.
 
1162
def _filter_revisions_touching_file_id(branch, file_id, view_revisions,
 
1163
    include_merges=True):
 
1164
    r"""Return the list of revision ids which touch a given file id.
1249
1165
 
1250
1166
    The function filters view_revisions and returns a subset.
1251
 
    This includes the revisions which directly change the path,
 
1167
    This includes the revisions which directly change the file id,
1252
1168
    and the revisions which merge these changes. So if the
1253
1169
    revision graph is::
1254
1170
 
1271
1187
 
1272
1188
    :param branch: The branch where we can get text revision information.
1273
1189
 
1274
 
    :param path: Filter out revisions that do not touch path.
 
1190
    :param file_id: Filter out revisions that do not touch file_id.
1275
1191
 
1276
1192
    :param view_revisions: A list of (revision_id, dotted_revno, merge_depth)
1277
1193
        tuples. This is the list of revisions which will be filtered. It is
1285
1201
    # Lookup all possible text keys to determine which ones actually modified
1286
1202
    # the file.
1287
1203
    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
1204
    get_parent_map = graph.get_parent_map
1291
1205
    text_keys = [(file_id, rev_id) for rev_id, revno, depth in view_revisions]
1292
1206
    next_keys = None
1333
1247
    """Reverse revisions by depth.
1334
1248
 
1335
1249
    Revisions with a different depth are sorted as a group with the previous
1336
 
    revision of that depth.  There may be no topological justification for this
 
1250
    revision of that depth.  There may be no topological justification for this,
1337
1251
    but it looks much nicer.
1338
1252
    """
1339
1253
    # Add a fake revision at start so that we can always attach sub revisions
1446
1360
        """
1447
1361
        self.to_file = to_file
1448
1362
        # 'exact' stream used to show diff, it should print content 'as is'
1449
 
        # and should not try to decode/encode it to unicode to avoid bug
1450
 
        # #328007
 
1363
        # and should not try to decode/encode it to unicode to avoid bug #328007
1451
1364
        if to_exact_file is not None:
1452
1365
            self.to_exact_file = to_exact_file
1453
1366
        else:
1454
 
            # XXX: somewhat hacky; this assumes it's a codec writer; it's
1455
 
            # better for code that expects to get diffs to pass in the exact
1456
 
            # file stream
 
1367
            # XXX: somewhat hacky; this assumes it's a codec writer; it's better
 
1368
            # for code that expects to get diffs to pass in the exact file
 
1369
            # stream
1457
1370
            self.to_exact_file = getattr(to_file, 'stream', to_file)
1458
1371
        self.show_ids = show_ids
1459
1372
        self.show_timezone = show_timezone
1460
1373
        if delta_format is None:
1461
1374
            # Ensures backward compatibility
1462
 
            delta_format = 2  # long format
 
1375
            delta_format = 2 # long format
1463
1376
        self.delta_format = delta_format
1464
1377
        self.levels = levels
1465
1378
        self._show_advice = show_advice
1563
1476
        """
1564
1477
        lines = self._foreign_info_properties(revision)
1565
1478
        for key, handler in properties_handler_registry.iteritems():
1566
 
            try:
1567
 
                lines.extend(self._format_properties(handler(revision)))
1568
 
            except Exception:
1569
 
                trace.log_exception_quietly()
1570
 
                trace.print_exception(sys.exc_info(), self.to_file)
 
1479
            lines.extend(self._format_properties(handler(revision)))
1571
1480
        return lines
1572
1481
 
1573
1482
    def _foreign_info_properties(self, rev):
1581
1490
                rev.mapping.vcs.show_foreign_revid(rev.foreign_revid))
1582
1491
 
1583
1492
        # Imported foreign revision revision ids always contain :
1584
 
        if b":" not in rev.revision_id:
 
1493
        if not ":" in rev.revision_id:
1585
1494
            return []
1586
1495
 
1587
1496
        # Revision was once imported from a foreign repository
1601
1510
        return lines
1602
1511
 
1603
1512
    def show_diff(self, to_file, diff, indent):
1604
 
        encoding = get_terminal_encoding()
1605
 
        for l in diff.rstrip().split(b'\n'):
1606
 
            to_file.write(indent + l.decode(encoding, 'ignore') + '\n')
 
1513
        for l in diff.rstrip().split('\n'):
 
1514
            to_file.write(indent + '%s\n' % (l,))
1607
1515
 
1608
1516
 
1609
1517
# Separator between revisions in long format
1632
1540
 
1633
1541
    def _date_string_original_timezone(self, rev):
1634
1542
        return format_date_with_offset_in_original_timezone(rev.timestamp,
1635
 
                                                            rev.timezone or 0)
 
1543
            rev.timezone or 0)
1636
1544
 
1637
1545
    def log_revision(self, revision):
1638
1546
        """Log a revision, either merged or not."""
1640
1548
        lines = [_LONG_SEP]
1641
1549
        if revision.revno is not None:
1642
1550
            lines.append('revno: %s%s' % (revision.revno,
1643
 
                                          self.merge_marker(revision)))
 
1551
                self.merge_marker(revision)))
1644
1552
        if revision.tags:
1645
 
            lines.append('tags: %s' % (', '.join(sorted(revision.tags))))
 
1553
            lines.append('tags: %s' % (', '.join(revision.tags)))
1646
1554
        if self.show_ids or revision.revno is None:
1647
 
            lines.append('revision-id: %s' %
1648
 
                         (revision.rev.revision_id.decode('utf-8'),))
 
1555
            lines.append('revision-id: %s' % (revision.rev.revision_id,))
1649
1556
        if self.show_ids:
1650
1557
            for parent_id in revision.rev.parent_ids:
1651
 
                lines.append('parent: %s' % (parent_id.decode('utf-8'),))
 
1558
                lines.append('parent: %s' % (parent_id,))
1652
1559
        lines.extend(self.custom_properties(revision.rev))
1653
1560
 
1654
1561
        committer = revision.rev.committer
1730
1637
        to_file = self.to_file
1731
1638
        tags = ''
1732
1639
        if revision.tags:
1733
 
            tags = ' {%s}' % (', '.join(sorted(revision.tags)))
 
1640
            tags = ' {%s}' % (', '.join(revision.tags))
1734
1641
        to_file.write(indent + "%*s %s\t%s%s%s\n" % (revno_width,
1735
 
                                                     revision.revno or "", self.short_author(
1736
 
                                                         revision.rev),
1737
 
                                                     format_date(revision.rev.timestamp,
1738
 
                                                                 revision.rev.timezone or 0,
1739
 
                                                                 self.show_timezone, date_fmt="%Y-%m-%d",
1740
 
                                                                 show_offset=False),
1741
 
                                                     tags, self.merge_marker(revision)))
1742
 
        self.show_properties(revision.rev, indent + offset)
 
1642
                revision.revno or "", self.short_author(revision.rev),
 
1643
                format_date(revision.rev.timestamp,
 
1644
                            revision.rev.timezone or 0,
 
1645
                            self.show_timezone, date_fmt="%Y-%m-%d",
 
1646
                            show_offset=False),
 
1647
                tags, self.merge_marker(revision)))
 
1648
        self.show_properties(revision.rev, indent+offset)
1743
1649
        if self.show_ids or revision.revno is None:
1744
1650
            to_file.write(indent + offset + 'revision-id:%s\n'
1745
 
                          % (revision.rev.revision_id.decode('utf-8'),))
 
1651
                          % (revision.rev.revision_id,))
1746
1652
        if not revision.rev.message:
1747
1653
            to_file.write(indent + offset + '(no message)\n')
1748
1654
        else:
1754
1660
            # Use the standard status output to display changes
1755
1661
            from breezy.delta import report_delta
1756
1662
            report_delta(to_file, revision.delta,
1757
 
                         short_status=self.delta_format == 1,
 
1663
                         short_status=self.delta_format==1,
1758
1664
                         show_ids=self.show_ids, indent=indent + offset)
1759
1665
        if revision.diff is not None:
1760
1666
            self.show_diff(self.to_exact_file, revision.diff, '      ')
1778
1684
    def truncate(self, str, max_len):
1779
1685
        if max_len is None or len(str) <= max_len:
1780
1686
            return str
1781
 
        return str[:max_len - 3] + '...'
 
1687
        return str[:max_len-3] + '...'
1782
1688
 
1783
1689
    def date_string(self, rev):
1784
1690
        return format_date(rev.timestamp, rev.timezone or 0,
1794
1700
    def log_revision(self, revision):
1795
1701
        indent = '  ' * revision.merge_depth
1796
1702
        self.to_file.write(self.log_string(revision.revno, revision.rev,
1797
 
                                           self._max_chars, revision.tags, indent))
 
1703
            self._max_chars, revision.tags, indent))
1798
1704
        self.to_file.write('\n')
1799
1705
 
1800
1706
    def log_string(self, revno, rev, max_chars, tags=None, prefix=''):
1813
1719
            # show revno only when is not None
1814
1720
            out.append("%s:" % revno)
1815
1721
        if max_chars is not None:
1816
 
            out.append(self.truncate(
1817
 
                self.short_author(rev), (max_chars + 3) // 4))
 
1722
            out.append(self.truncate(self.short_author(rev), (max_chars+3)/4))
1818
1723
        else:
1819
1724
            out.append(self.short_author(rev))
1820
1725
        out.append(self.date_string(rev))
1821
1726
        if len(rev.parent_ids) > 1:
1822
1727
            out.append('[merge]')
1823
1728
        if tags:
1824
 
            tag_str = '{%s}' % (', '.join(sorted(tags)))
 
1729
            tag_str = '{%s}' % (', '.join(tags))
1825
1730
            out.append(tag_str)
1826
1731
        out.append(rev.get_summary())
1827
1732
        return self.truncate(prefix + " ".join(out).rstrip('\n'), max_chars)
1843
1748
                               show_offset=False)
1844
1749
        committer_str = self.authors(revision.rev, 'first', sep=', ')
1845
1750
        committer_str = committer_str.replace(' <', '  <')
1846
 
        to_file.write('%s  %s\n\n' % (date_str, committer_str))
 
1751
        to_file.write('%s  %s\n\n' % (date_str,committer_str))
1847
1752
 
1848
1753
        if revision.delta is not None and revision.delta.has_changed():
1849
1754
            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]
 
1755
                path, = c[:1]
1854
1756
                to_file.write('\t* %s:\n' % (path,))
1855
 
            for c in revision.delta.renamed + revision.delta.copied:
 
1757
            for c in revision.delta.renamed:
 
1758
                oldpath,newpath = c[:2]
1856
1759
                # 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]))
 
1760
                to_file.write('\t* %s:\n\t* %s:\n' % (oldpath,newpath))
1858
1761
            to_file.write('\n')
1859
1762
 
1860
1763
        if not revision.rev.message:
1913
1816
    try:
1914
1817
        return log_formatter_registry.make_formatter(name, *args, **kwargs)
1915
1818
    except KeyError:
1916
 
        raise errors.CommandError(
1917
 
            gettext("unknown log formatter: %r") % name)
 
1819
        raise errors.BzrCommandError(gettext("unknown log formatter: %r") % name)
1918
1820
 
1919
1821
 
1920
1822
def author_list_all(rev):
1956
1858
    """
1957
1859
    if to_file is None:
1958
1860
        to_file = codecs.getwriter(get_terminal_encoding())(sys.stdout,
1959
 
                                                            errors='replace')
 
1861
            errors='replace')
1960
1862
    lf = log_formatter(log_format,
1961
1863
                       show_ids=False,
1962
1864
                       to_file=to_file,
1968
1870
    for i in range(max(len(new_rh), len(old_rh))):
1969
1871
        if (len(new_rh) <= i
1970
1872
            or len(old_rh) <= i
1971
 
                or new_rh[i] != old_rh[i]):
 
1873
            or new_rh[i] != old_rh[i]):
1972
1874
            base_idx = i
1973
1875
            break
1974
1876
 
1975
1877
    if base_idx is None:
1976
1878
        to_file.write('Nothing seems to have changed\n')
1977
1879
        return
1978
 
    # TODO: It might be nice to do something like show_log
1979
 
    # and show the merged entries. But since this is the
1980
 
    # removed revisions, it shouldn't be as important
 
1880
    ## TODO: It might be nice to do something like show_log
 
1881
    ##       and show the merged entries. But since this is the
 
1882
    ##       removed revisions, it shouldn't be as important
1981
1883
    if base_idx < len(old_rh):
1982
 
        to_file.write('*' * 60)
 
1884
        to_file.write('*'*60)
1983
1885
        to_file.write('\nRemoved Revisions:\n')
1984
1886
        for i in range(base_idx, len(old_rh)):
1985
1887
            rev = branch.repository.get_revision(old_rh[i])
1986
 
            lr = LogRevision(rev, i + 1, 0, None)
 
1888
            lr = LogRevision(rev, i+1, 0, None)
1987
1889
            lf.log_revision(lr)
1988
 
        to_file.write('*' * 60)
 
1890
        to_file.write('*'*60)
1989
1891
        to_file.write('\n\n')
1990
1892
    if base_idx < len(new_rh):
1991
1893
        to_file.write('Added Revisions:\n')
1992
1894
        show_log(branch,
1993
1895
                 lf,
 
1896
                 None,
1994
1897
                 verbose=False,
1995
1898
                 direction='forward',
1996
 
                 start_revision=base_idx + 1,
1997
 
                 end_revision=len(new_rh))
 
1899
                 start_revision=base_idx+1,
 
1900
                 end_revision=len(new_rh),
 
1901
                 search=None)
1998
1902
 
1999
1903
 
2000
1904
def get_history_change(old_revision_id, new_revision_id, repository):
2066
1970
    log_format = log_formatter_registry.get_default(branch)
2067
1971
    lf = log_format(show_ids=False, to_file=output, show_timezone='original')
2068
1972
    if old_history != []:
2069
 
        output.write('*' * 60)
 
1973
        output.write('*'*60)
2070
1974
        output.write('\nRemoved Revisions:\n')
2071
1975
        show_flat_log(branch.repository, old_history, old_revno, lf)
2072
 
        output.write('*' * 60)
 
1976
        output.write('*'*60)
2073
1977
        output.write('\n\n')
2074
1978
    if new_history != []:
2075
1979
        output.write('Added Revisions:\n')
2076
1980
        start_revno = new_revno - len(new_history) + 1
2077
 
        show_log(branch, lf, verbose=False, direction='forward',
2078
 
                 start_revision=start_revno)
 
1981
        show_log(branch, lf, None, verbose=False, direction='forward',
 
1982
                 start_revision=start_revno,)
2079
1983
 
2080
1984
 
2081
1985
def show_flat_log(repository, history, last_revno, lf):
2086
1990
    :param last_revno: The revno of the last revision_id in the history.
2087
1991
    :param lf: The log formatter to use.
2088
1992
    """
 
1993
    start_revno = last_revno - len(history) + 1
2089
1994
    revisions = repository.get_revisions(history)
2090
1995
    for i, rev in enumerate(revisions):
2091
1996
        lr = LogRevision(rev, i + last_revno, 0, None)
2092
1997
        lf.log_revision(lr)
2093
1998
 
2094
1999
 
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.
 
2000
def _get_info_for_log_files(revisionspec_list, file_list, add_cleanup):
 
2001
    """Find file-ids and kinds given a list of files and a revision range.
2097
2002
 
2098
2003
    We search for files at the end of the range. If not found there,
2099
2004
    we try the start of the range.
2102
2007
    :param file_list: the list of paths given on the command line;
2103
2008
      the first of these can be a branch location or a file path,
2104
2009
      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.
 
2010
    :param add_cleanup: When the branch returned is read locked,
 
2011
      an unlock call will be queued to the cleanup.
2107
2012
    :return: (branch, info_list, start_rev_info, end_rev_info) where
2108
 
      info_list is a list of (relative_path, found, kind) tuples where
 
2013
      info_list is a list of (relative_path, file_id, kind) tuples where
2109
2014
      kind is one of values 'directory', 'file', 'symlink', 'tree-reference'.
2110
2015
      branch will be read-locked.
2111
2016
    """
2112
2017
    from breezy.builtins import _get_revision_range
2113
2018
    tree, b, path = controldir.ControlDir.open_containing_tree_or_branch(
2114
2019
        file_list[0])
2115
 
    exit_stack.enter_context(b.lock_read())
 
2020
    add_cleanup(b.lock_read().unlock)
2116
2021
    # XXX: It's damn messy converting a list of paths to relative paths when
2117
2022
    # those paths might be deleted ones, they might be on a case-insensitive
2118
2023
    # filesystem and/or they might be in silly locations (like another branch).
2127
2032
        relpaths = [path] + file_list[1:]
2128
2033
    info_list = []
2129
2034
    start_rev_info, end_rev_info = _get_revision_range(revisionspec_list, b,
2130
 
                                                       "log")
 
2035
        "log")
2131
2036
    if relpaths in ([], [u'']):
2132
2037
        return b, [], start_rev_info, end_rev_info
2133
2038
    if start_rev_info is None and end_rev_info is None:
2135
2040
            tree = b.basis_tree()
2136
2041
        tree1 = None
2137
2042
        for fp in relpaths:
2138
 
            kind = _get_kind_for_file(tree, fp)
2139
 
            if not kind:
 
2043
            file_id = tree.path2id(fp)
 
2044
            kind = _get_kind_for_file_id(tree, file_id)
 
2045
            if file_id is None:
2140
2046
                # go back to when time began
2141
2047
                if tree1 is None:
2142
2048
                    try:
2143
2049
                        rev1 = b.get_rev_id(1)
2144
2050
                    except errors.NoSuchRevision:
2145
2051
                        # No history at all
 
2052
                        file_id = None
2146
2053
                        kind = None
2147
2054
                    else:
2148
2055
                        tree1 = b.repository.revision_tree(rev1)
2149
2056
                if tree1:
2150
 
                    kind = _get_kind_for_file(tree1, fp)
2151
 
            info_list.append((fp, kind))
 
2057
                    file_id = tree1.path2id(fp)
 
2058
                    kind = _get_kind_for_file_id(tree1, file_id)
 
2059
            info_list.append((fp, file_id, kind))
2152
2060
 
2153
2061
    elif start_rev_info == end_rev_info:
2154
2062
        # One revision given - file must exist in it
2155
2063
        tree = b.repository.revision_tree(end_rev_info.rev_id)
2156
2064
        for fp in relpaths:
2157
 
            kind = _get_kind_for_file(tree, fp)
2158
 
            info_list.append((fp, kind))
 
2065
            file_id = tree.path2id(fp)
 
2066
            kind = _get_kind_for_file_id(tree, file_id)
 
2067
            info_list.append((fp, file_id, kind))
2159
2068
 
2160
2069
    else:
2161
2070
        # Revision range given. Get the file-id from the end tree.
2167
2076
            tree = b.repository.revision_tree(rev_id)
2168
2077
        tree1 = None
2169
2078
        for fp in relpaths:
2170
 
            kind = _get_kind_for_file(tree, fp)
2171
 
            if not kind:
 
2079
            file_id = tree.path2id(fp)
 
2080
            kind = _get_kind_for_file_id(tree, file_id)
 
2081
            if file_id is None:
2172
2082
                if tree1 is None:
2173
2083
                    rev_id = start_rev_info.rev_id
2174
2084
                    if rev_id is None:
2176
2086
                        tree1 = b.repository.revision_tree(rev1)
2177
2087
                    else:
2178
2088
                        tree1 = b.repository.revision_tree(rev_id)
2179
 
                kind = _get_kind_for_file(tree1, fp)
2180
 
            info_list.append((fp, kind))
 
2089
                file_id = tree1.path2id(fp)
 
2090
                kind = _get_kind_for_file_id(tree1, file_id)
 
2091
            info_list.append((fp, file_id, kind))
2181
2092
    return b, info_list, start_rev_info, end_rev_info
2182
2093
 
2183
2094
 
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
 
2095
def _get_kind_for_file_id(tree, file_id):
 
2096
    """Return the kind of a file-id or None if it doesn't exist."""
 
2097
    if file_id is not None:
 
2098
        return tree.kind(file_id)
 
2099
    else:
 
2100
        return None
2191
2101
 
2192
2102
 
2193
2103
properties_handler_registry = registry.Registry()
2194
2104
 
2195
2105
# Use the properties handlers to print out bug information if available
2196
 
 
2197
 
 
2198
2106
def _bugs_properties_handler(revision):
2199
 
    fixed_bug_urls = []
2200
 
    related_bug_urls = []
2201
 
    for bug_url, status in revision.iter_bugs():
2202
 
        if status == 'fixed':
2203
 
            fixed_bug_urls.append(bug_url)
2204
 
        elif status == 'related':
2205
 
            related_bug_urls.append(bug_url)
2206
 
    ret = {}
2207
 
    if fixed_bug_urls:
2208
 
        text = ngettext('fixes bug', 'fixes bugs', len(fixed_bug_urls))
2209
 
        ret[text] = ' '.join(fixed_bug_urls)
2210
 
    if related_bug_urls:
2211
 
        text = ngettext('related bug', 'related bugs',
2212
 
                        len(related_bug_urls))
2213
 
        ret[text] = ' '.join(related_bug_urls)
2214
 
    return ret
 
2107
    if 'bugs' in revision.properties:
 
2108
        bug_lines = revision.properties['bugs'].split('\n')
 
2109
        bug_rows = [line.split(' ', 1) for line in bug_lines]
 
2110
        fixed_bug_urls = [row[0] for row in bug_rows if
 
2111
                          len(row) > 1 and row[1] == 'fixed']
2215
2112
 
 
2113
        if fixed_bug_urls:
 
2114
            return {ngettext('fixes bug', 'fixes bugs', len(fixed_bug_urls)):\
 
2115
                    ' '.join(fixed_bug_urls)}
 
2116
    return {}
2216
2117
 
2217
2118
properties_handler_registry.register('bugs_properties_handler',
2218
2119
                                     _bugs_properties_handler)