/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-08-12 20:24:50 UTC
  • mto: (7290.1.35 work)
  • mto: This revision was merged to the branch mainline in revision 7405.
  • Revision ID: jelmer@jelmer.uk-20190812202450-vdpamxay6sebo93w
Fix path to brz.

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# Copyright (C) 2005-2011 Canonical Ltd
 
2
#
 
3
# This program is free software; you can redistribute it and/or modify
 
4
# it under the terms of the GNU General Public License as published by
 
5
# the Free Software Foundation; either version 2 of the License, or
 
6
# (at your option) any later version.
 
7
#
 
8
# This program is distributed in the hope that it will be useful,
 
9
# but WITHOUT ANY WARRANTY; without even the implied warranty of
 
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 
11
# GNU General Public License for more details.
 
12
#
 
13
# You should have received a copy of the GNU General Public License
 
14
# along with this program; if not, write to the Free Software
 
15
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
 
16
 
 
17
"""Code to show logs of changes.
 
18
 
 
19
Various flavors of log can be produced:
 
20
 
 
21
* for one file, or the whole tree, and (not done yet) for
 
22
  files in a given directory
 
23
 
 
24
* in "verbose" mode with a description of what changed from one
 
25
  version to the next
 
26
 
 
27
* with file-ids and revision-ids shown
 
28
 
 
29
Logs are actually written out through an abstract LogFormatter
 
30
interface, which allows for different preferred formats.  Plugins can
 
31
register formats too.
 
32
 
 
33
Logs can be produced in either forward (oldest->newest) or reverse
 
34
(newest->oldest) order.
 
35
 
 
36
Logs can be filtered to show only revisions matching a particular
 
37
search string, or within a particular range of revisions.  The range
 
38
can be given as date/times, which are reduced to revisions before
 
39
calling in here.
 
40
 
 
41
In verbose mode we show a summary of what changed in each particular
 
42
revision.  Note that this is the delta for changes in that revision
 
43
relative to its left-most parent, not the delta relative to the last
 
44
logged revision.  So for example if you ask for a verbose log of
 
45
changes touching hello.c you will get a list of those revisions also
 
46
listing other things that were changed in the same revision, but not
 
47
all the changes since the previous revision that touched hello.c.
 
48
"""
 
49
 
 
50
from __future__ import absolute_import
 
51
 
 
52
import codecs
 
53
import itertools
 
54
import re
 
55
import sys
 
56
from warnings import (
 
57
    warn,
 
58
    )
 
59
 
 
60
from .lazy_import import lazy_import
 
61
lazy_import(globals(), """
 
62
 
 
63
from breezy import (
 
64
    config,
 
65
    controldir,
 
66
    diff,
 
67
    foreign,
 
68
    lazy_regex,
 
69
    revision as _mod_revision,
 
70
    )
 
71
from breezy.i18n import gettext, ngettext
 
72
""")
 
73
 
 
74
from . import (
 
75
    errors,
 
76
    registry,
 
77
    revisionspec,
 
78
    trace,
 
79
    )
 
80
from .osutils import (
 
81
    format_date,
 
82
    format_date_with_offset_in_original_timezone,
 
83
    get_diff_header_encoding,
 
84
    get_terminal_encoding,
 
85
    terminal_width,
 
86
    )
 
87
from .sixish import (
 
88
    BytesIO,
 
89
    range,
 
90
    zip,
 
91
    )
 
92
from .tree import find_previous_path
 
93
 
 
94
 
 
95
def find_touching_revisions(repository, last_revision, last_tree, last_path):
 
96
    """Yield a description of revisions which affect the file_id.
 
97
 
 
98
    Each returned element is (revno, revision_id, description)
 
99
 
 
100
    This is the list of revisions where the file is either added,
 
101
    modified, renamed or deleted.
 
102
 
 
103
    TODO: Perhaps some way to limit this to only particular revisions,
 
104
    or to traverse a non-mainline set of revisions?
 
105
    """
 
106
    last_verifier = last_tree.get_file_verifier(last_path)
 
107
    graph = repository.get_graph()
 
108
    history = list(graph.iter_lefthand_ancestry(last_revision, []))
 
109
    revno = len(history)
 
110
    for revision_id in history:
 
111
        this_tree = repository.revision_tree(revision_id)
 
112
        this_path = find_previous_path(last_tree, this_tree, last_path)
 
113
 
 
114
        # now we know how it was last time, and how it is in this revision.
 
115
        # are those two states effectively the same or not?
 
116
        if this_path is not None and last_path is None:
 
117
            yield revno, revision_id, "deleted " + this_path
 
118
            this_verifier = this_tree.get_file_verifier(this_path)
 
119
        elif this_path is None and last_path is not None:
 
120
            yield revno, revision_id, "added " + last_path
 
121
        elif this_path != last_path:
 
122
            yield revno, revision_id, ("renamed %s => %s" % (this_path, last_path))
 
123
            this_verifier = this_tree.get_file_verifier(this_path)
 
124
        else:
 
125
            this_verifier = this_tree.get_file_verifier(this_path)
 
126
            if (this_verifier != last_verifier):
 
127
                yield revno, revision_id, "modified " + this_path
 
128
 
 
129
        last_verifier = this_verifier
 
130
        last_path = this_path
 
131
        last_tree = this_tree
 
132
        if last_path is None:
 
133
            return
 
134
        revno -= 1
 
135
 
 
136
 
 
137
def show_log(branch,
 
138
             lf,
 
139
             specific_fileid=None,
 
140
             verbose=False,
 
141
             direction='reverse',
 
142
             start_revision=None,
 
143
             end_revision=None,
 
144
             search=None,
 
145
             limit=None,
 
146
             show_diff=False,
 
147
             match=None):
 
148
    """Write out human-readable log of commits to this branch.
 
149
 
 
150
    This function is being retained for backwards compatibility but
 
151
    should not be extended with new parameters. Use the new Logger class
 
152
    instead, eg. Logger(branch, rqst).show(lf), adding parameters to the
 
153
    make_log_request_dict function.
 
154
 
 
155
    :param lf: The LogFormatter object showing the output.
 
156
 
 
157
    :param specific_fileid: If not None, list only the commits affecting the
 
158
        specified file, rather than all commits.
 
159
 
 
160
    :param verbose: If True show added/changed/deleted/renamed files.
 
161
 
 
162
    :param direction: 'reverse' (default) is latest to earliest; 'forward' is
 
163
        earliest to latest.
 
164
 
 
165
    :param start_revision: If not None, only show revisions >= start_revision
 
166
 
 
167
    :param end_revision: If not None, only show revisions <= end_revision
 
168
 
 
169
    :param search: If not None, only show revisions with matching commit
 
170
        messages
 
171
 
 
172
    :param limit: If set, shows only 'limit' revisions, all revisions are shown
 
173
        if None or 0.
 
174
 
 
175
    :param show_diff: If True, output a diff after each revision.
 
176
 
 
177
    :param match: Dictionary of search lists to use when matching revision
 
178
      properties.
 
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
 
185
    if verbose:
 
186
        if file_ids:
 
187
            delta_type = 'partial'
 
188
        else:
 
189
            delta_type = 'full'
 
190
    else:
 
191
        delta_type = None
 
192
    if show_diff:
 
193
        if file_ids:
 
194
            diff_type = 'partial'
 
195
        else:
 
196
            diff_type = 'full'
 
197
    else:
 
198
        diff_type = None
 
199
 
 
200
    if isinstance(start_revision, int):
 
201
        try:
 
202
            start_revision = revisionspec.RevisionInfo(branch, start_revision)
 
203
        except (errors.NoSuchRevision, errors.RevnoOutOfBounds):
 
204
            raise errors.InvalidRevisionNumber(start_revision)
 
205
 
 
206
    if isinstance(end_revision, int):
 
207
        try:
 
208
            end_revision = revisionspec.RevisionInfo(branch, end_revision)
 
209
        except (errors.NoSuchRevision, errors.RevnoOutOfBounds):
 
210
            raise errors.InvalidRevisionNumber(end_revision)
 
211
 
 
212
    if end_revision is not None and end_revision.revno == 0:
 
213
        raise errors.InvalidRevisionNumber(end_revision.revno)
 
214
 
 
215
    # Build the request and execute it
 
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)
 
220
    Logger(branch, rqst).show(lf)
 
221
 
 
222
 
 
223
# Note: This needs to be kept in sync with the defaults in
 
224
# make_log_request_dict() below
 
225
_DEFAULT_REQUEST_PARAMS = {
 
226
    'direction': 'reverse',
 
227
    'levels': None,
 
228
    'generate_tags': True,
 
229
    'exclude_common_ancestry': False,
 
230
    '_match_using_deltas': True,
 
231
    }
 
232
 
 
233
 
 
234
def make_log_request_dict(direction='reverse', specific_fileids=None,
 
235
                          start_revision=None, end_revision=None, limit=None,
 
236
                          message_search=None, levels=None, generate_tags=True,
 
237
                          delta_type=None,
 
238
                          diff_type=None, _match_using_deltas=True,
 
239
                          exclude_common_ancestry=False, match=None,
 
240
                          signature=False, omit_merges=False,
 
241
                          ):
 
242
    """Convenience function for making a logging request dictionary.
 
243
 
 
244
    Using this function may make code slightly safer by ensuring
 
245
    parameters have the correct names. It also provides a reference
 
246
    point for documenting the supported parameters.
 
247
 
 
248
    :param direction: 'reverse' (default) is latest to earliest;
 
249
      'forward' is earliest to latest.
 
250
 
 
251
    :param specific_fileids: If not None, only include revisions
 
252
      affecting the specified files, rather than all revisions.
 
253
 
 
254
    :param start_revision: If not None, only generate
 
255
      revisions >= start_revision
 
256
 
 
257
    :param end_revision: If not None, only generate
 
258
      revisions <= end_revision
 
259
 
 
260
    :param limit: If set, generate only 'limit' revisions, all revisions
 
261
      are shown if None or 0.
 
262
 
 
263
    :param message_search: If not None, only include revisions with
 
264
      matching commit messages
 
265
 
 
266
    :param levels: the number of levels of revisions to
 
267
      generate; 1 for just the mainline; 0 for all levels, or None for
 
268
      a sensible default.
 
269
 
 
270
    :param generate_tags: If True, include tags for matched revisions.
 
271
`
 
272
    :param delta_type: Either 'full', 'partial' or None.
 
273
      'full' means generate the complete delta - adds/deletes/modifies/etc;
 
274
      'partial' means filter the delta using specific_fileids;
 
275
      None means do not generate any delta.
 
276
 
 
277
    :param diff_type: Either 'full', 'partial' or None.
 
278
      'full' means generate the complete diff - adds/deletes/modifies/etc;
 
279
      'partial' means filter the diff using specific_fileids;
 
280
      None means do not generate any diff.
 
281
 
 
282
    :param _match_using_deltas: a private parameter controlling the
 
283
      algorithm used for matching specific_fileids. This parameter
 
284
      may be removed in the future so breezy client code should NOT
 
285
      use it.
 
286
 
 
287
    :param exclude_common_ancestry: Whether -rX..Y should be interpreted as a
 
288
      range operator or as a graph difference.
 
289
 
 
290
    :param signature: show digital signature information
 
291
 
 
292
    :param match: Dictionary of list of search strings to use when filtering
 
293
      revisions. Keys can be 'message', 'author', 'committer', 'bugs' or
 
294
      the empty string to match any of the preceding properties.
 
295
 
 
296
    :param omit_merges: If True, commits with more than one parent are
 
297
      omitted.
 
298
 
 
299
    """
 
300
    # Take care of old style message_search parameter
 
301
    if message_search:
 
302
        if match:
 
303
            if 'message' in match:
 
304
                match['message'].append(message_search)
 
305
            else:
 
306
                match['message'] = [message_search]
 
307
        else:
 
308
            match = {'message': [message_search]}
 
309
    return {
 
310
        'direction': direction,
 
311
        'specific_fileids': specific_fileids,
 
312
        'start_revision': start_revision,
 
313
        'end_revision': end_revision,
 
314
        'limit': limit,
 
315
        'levels': levels,
 
316
        'generate_tags': generate_tags,
 
317
        'delta_type': delta_type,
 
318
        'diff_type': diff_type,
 
319
        'exclude_common_ancestry': exclude_common_ancestry,
 
320
        'signature': signature,
 
321
        'match': match,
 
322
        'omit_merges': omit_merges,
 
323
        # Add 'private' attributes for features that may be deprecated
 
324
        '_match_using_deltas': _match_using_deltas,
 
325
    }
 
326
 
 
327
 
 
328
def _apply_log_request_defaults(rqst):
 
329
    """Apply default values to a request dictionary."""
 
330
    result = _DEFAULT_REQUEST_PARAMS.copy()
 
331
    if rqst:
 
332
        result.update(rqst)
 
333
    return result
 
334
 
 
335
 
 
336
def format_signature_validity(rev_id, branch):
 
337
    """get the signature validity
 
338
 
 
339
    :param rev_id: revision id to validate
 
340
    :param branch: branch of revision
 
341
    :return: human readable string to print to log
 
342
    """
 
343
    from breezy import gpg
 
344
 
 
345
    gpg_strategy = gpg.GPGStrategy(branch.get_config_stack())
 
346
    result = branch.repository.verify_revision_signature(rev_id, gpg_strategy)
 
347
    if result[0] == gpg.SIGNATURE_VALID:
 
348
        return u"valid signature from {0}".format(result[1])
 
349
    if result[0] == gpg.SIGNATURE_KEY_MISSING:
 
350
        return "unknown key {0}".format(result[1])
 
351
    if result[0] == gpg.SIGNATURE_NOT_VALID:
 
352
        return "invalid signature!"
 
353
    if result[0] == gpg.SIGNATURE_NOT_SIGNED:
 
354
        return "no signature"
 
355
 
 
356
 
 
357
class LogGenerator(object):
 
358
    """A generator of log revisions."""
 
359
 
 
360
    def iter_log_revisions(self):
 
361
        """Iterate over LogRevision objects.
 
362
 
 
363
        :return: An iterator yielding LogRevision objects.
 
364
        """
 
365
        raise NotImplementedError(self.iter_log_revisions)
 
366
 
 
367
 
 
368
class Logger(object):
 
369
    """An object that generates, formats and displays a log."""
 
370
 
 
371
    def __init__(self, branch, rqst):
 
372
        """Create a Logger.
 
373
 
 
374
        :param branch: the branch to log
 
375
        :param rqst: A dictionary specifying the query parameters.
 
376
          See make_log_request_dict() for supported values.
 
377
        """
 
378
        self.branch = branch
 
379
        self.rqst = _apply_log_request_defaults(rqst)
 
380
 
 
381
    def show(self, lf):
 
382
        """Display the log.
 
383
 
 
384
        :param lf: The LogFormatter object to send the output to.
 
385
        """
 
386
        if not isinstance(lf, LogFormatter):
 
387
            warn("not a LogFormatter instance: %r" % lf)
 
388
 
 
389
        with self.branch.lock_read():
 
390
            if getattr(lf, 'begin_log', None):
 
391
                lf.begin_log()
 
392
            self._show_body(lf)
 
393
            if getattr(lf, 'end_log', None):
 
394
                lf.end_log()
 
395
 
 
396
    def _show_body(self, lf):
 
397
        """Show the main log output.
 
398
 
 
399
        Subclasses may wish to override this.
 
400
        """
 
401
        # Tweak the LogRequest based on what the LogFormatter can handle.
 
402
        # (There's no point generating stuff if the formatter can't display it.)
 
403
        rqst = self.rqst
 
404
        if rqst['levels'] is None or lf.get_levels() > rqst['levels']:
 
405
            # user didn't specify levels, use whatever the LF can handle:
 
406
            rqst['levels'] = lf.get_levels()
 
407
 
 
408
        if not getattr(lf, 'supports_tags', False):
 
409
            rqst['generate_tags'] = False
 
410
        if not getattr(lf, 'supports_delta', False):
 
411
            rqst['delta_type'] = None
 
412
        if not getattr(lf, 'supports_diff', False):
 
413
            rqst['diff_type'] = None
 
414
        if not getattr(lf, 'supports_signatures', False):
 
415
            rqst['signature'] = False
 
416
 
 
417
        # Find and print the interesting revisions
 
418
        generator = self._generator_factory(self.branch, rqst)
 
419
        try:
 
420
            for lr in generator.iter_log_revisions():
 
421
                lf.log_revision(lr)
 
422
        except errors.GhostRevisionUnusableHere:
 
423
            raise errors.BzrCommandError(
 
424
                gettext('Further revision history missing.'))
 
425
        lf.show_advice()
 
426
 
 
427
    def _generator_factory(self, branch, rqst):
 
428
        """Make the LogGenerator object to use.
 
429
 
 
430
        Subclasses may wish to override this.
 
431
        """
 
432
        return _DefaultLogGenerator(branch, rqst)
 
433
 
 
434
 
 
435
class _StartNotLinearAncestor(Exception):
 
436
    """Raised when a start revision is not found walking left-hand history."""
 
437
 
 
438
 
 
439
class _DefaultLogGenerator(LogGenerator):
 
440
    """The default generator of log revisions."""
 
441
 
 
442
    def __init__(self, branch, rqst):
 
443
        self.branch = branch
 
444
        self.rqst = rqst
 
445
        if rqst.get('generate_tags') and branch.supports_tags():
 
446
            self.rev_tag_dict = branch.tags.get_reverse_tag_dict()
 
447
        else:
 
448
            self.rev_tag_dict = {}
 
449
 
 
450
    def iter_log_revisions(self):
 
451
        """Iterate over LogRevision objects.
 
452
 
 
453
        :return: An iterator yielding LogRevision objects.
 
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')
 
461
        log_count = 0
 
462
        revision_iterator = self._create_log_revision_iterator()
 
463
        for revs in revision_iterator:
 
464
            for (rev_id, revno, merge_depth), rev, delta in revs:
 
465
                # 0 levels means show everything; merge_depth counts from 0
 
466
                if (levels != 0 and merge_depth is not None and
 
467
                        merge_depth >= levels):
 
468
                    continue
 
469
                if omit_merges and len(rev.parent_ids) > 1:
 
470
                    continue
 
471
                if rev is None:
 
472
                    raise errors.GhostRevisionUnusableHere(rev_id)
 
473
                if diff_type is None:
 
474
                    diff = None
 
475
                else:
 
476
                    diff = self._format_diff(rev, rev_id, diff_type)
 
477
                if show_signature:
 
478
                    signature = format_signature_validity(rev_id, self.branch)
 
479
                else:
 
480
                    signature = None
 
481
                yield LogRevision(
 
482
                    rev, revno, merge_depth, delta,
 
483
                    self.rev_tag_dict.get(rev_id), diff, signature)
 
484
                if limit:
 
485
                    log_count += 1
 
486
                    if log_count >= limit:
 
487
                        return
 
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
 
 
508
    def _create_log_revision_iterator(self):
 
509
        """Create a revision iterator for log.
 
510
 
 
511
        :return: An iterator over lists of ((rev_id, revno, merge_depth), rev,
 
512
            delta).
 
513
        """
 
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()
 
519
        else:
 
520
            # We're using the per-file-graph algorithm. This scales really
 
521
            # well but only makes sense if there is a single file and it's
 
522
            # not a directory
 
523
            file_count = len(self.rqst.get('specific_fileids'))
 
524
            if file_count != 1:
 
525
                raise errors.BzrError(
 
526
                    "illegal LogRequest: must match-using-deltas "
 
527
                    "when logging %d files" % file_count)
 
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'))
 
566
 
 
567
 
 
568
def _calc_view_revisions(branch, start_rev_id, end_rev_id, direction,
 
569
                         generate_merge_revisions,
 
570
                         delayed_graph_generation=False,
 
571
                         exclude_common_ancestry=False,
 
572
                         ):
 
573
    """Calculate the revisions to view.
 
574
 
 
575
    :return: An iterator of (revision_id, dotted_revno, merge_depth) tuples OR
 
576
             a list of the same tuples.
 
577
    """
 
578
    if (exclude_common_ancestry and start_rev_id == end_rev_id):
 
579
        raise errors.BzrCommandError(gettext(
 
580
            '--exclude-common-ancestry requires two different revisions'))
 
581
    if direction not in ('reverse', 'forward'):
 
582
        raise ValueError(gettext('invalid direction %r') % direction)
 
583
    br_rev_id = branch.last_revision()
 
584
    if br_rev_id == _mod_revision.NULL_REVISION:
 
585
        return []
 
586
 
 
587
    if (end_rev_id and start_rev_id == end_rev_id
 
588
        and (not generate_merge_revisions
 
589
             or not _has_merges(branch, end_rev_id))):
 
590
        # If a single revision is requested, check we can handle it
 
591
        return _generate_one_revision(branch, end_rev_id, br_rev_id,
 
592
                                      branch.revno())
 
593
    if not generate_merge_revisions:
 
594
        try:
 
595
            # If we only want to see linear revisions, we can iterate ...
 
596
            iter_revs = _linear_view_revisions(
 
597
                branch, start_rev_id, end_rev_id,
 
598
                exclude_common_ancestry=exclude_common_ancestry)
 
599
            # If a start limit was given and it's not obviously an
 
600
            # ancestor of the end limit, check it before outputting anything
 
601
            if (direction == 'forward'
 
602
                or (start_rev_id and not _is_obvious_ancestor(
 
603
                    branch, start_rev_id, end_rev_id))):
 
604
                iter_revs = list(iter_revs)
 
605
            if direction == 'forward':
 
606
                iter_revs = reversed(iter_revs)
 
607
            return iter_revs
 
608
        except _StartNotLinearAncestor:
 
609
            # Switch to the slower implementation that may be able to find a
 
610
            # non-obvious ancestor out of the left-hand history.
 
611
            pass
 
612
    iter_revs = _generate_all_revisions(branch, start_rev_id, end_rev_id,
 
613
                                        direction, delayed_graph_generation,
 
614
                                        exclude_common_ancestry)
 
615
    if direction == 'forward':
 
616
        iter_revs = _rebase_merge_depth(reverse_by_depth(list(iter_revs)))
 
617
    return iter_revs
 
618
 
 
619
 
 
620
def _generate_one_revision(branch, rev_id, br_rev_id, br_revno):
 
621
    if rev_id == br_rev_id:
 
622
        # It's the tip
 
623
        return [(br_rev_id, br_revno, 0)]
 
624
    else:
 
625
        revno_str = _compute_revno_str(branch, rev_id)
 
626
        return [(rev_id, revno_str, 0)]
 
627
 
 
628
 
 
629
def _generate_all_revisions(branch, start_rev_id, end_rev_id, direction,
 
630
                            delayed_graph_generation,
 
631
                            exclude_common_ancestry=False):
 
632
    # On large trees, generating the merge graph can take 30-60 seconds
 
633
    # so we delay doing it until a merge is detected, incrementally
 
634
    # returning initial (non-merge) revisions while we can.
 
635
 
 
636
    # The above is only true for old formats (<= 0.92), for newer formats, a
 
637
    # couple of seconds only should be needed to load the whole graph and the
 
638
    # other graph operations needed are even faster than that -- vila 100201
 
639
    initial_revisions = []
 
640
    if delayed_graph_generation:
 
641
        try:
 
642
            for rev_id, revno, depth in _linear_view_revisions(
 
643
                    branch, start_rev_id, end_rev_id, exclude_common_ancestry):
 
644
                if _has_merges(branch, rev_id):
 
645
                    # The end_rev_id can be nested down somewhere. We need an
 
646
                    # explicit ancestry check. There is an ambiguity here as we
 
647
                    # may not raise _StartNotLinearAncestor for a revision that
 
648
                    # is an ancestor but not a *linear* one. But since we have
 
649
                    # loaded the graph to do the check (or calculate a dotted
 
650
                    # revno), we may as well accept to show the log...  We need
 
651
                    # the check only if start_rev_id is not None as all
 
652
                    # revisions have _mod_revision.NULL_REVISION as an ancestor
 
653
                    # -- vila 20100319
 
654
                    graph = branch.repository.get_graph()
 
655
                    if (start_rev_id is not None
 
656
                            and not graph.is_ancestor(start_rev_id, end_rev_id)):
 
657
                        raise _StartNotLinearAncestor()
 
658
                    # Since we collected the revisions so far, we need to
 
659
                    # adjust end_rev_id.
 
660
                    end_rev_id = rev_id
 
661
                    break
 
662
                else:
 
663
                    initial_revisions.append((rev_id, revno, depth))
 
664
            else:
 
665
                # No merged revisions found
 
666
                return initial_revisions
 
667
        except _StartNotLinearAncestor:
 
668
            # A merge was never detected so the lower revision limit can't
 
669
            # be nested down somewhere
 
670
            raise errors.BzrCommandError(gettext('Start revision not found in'
 
671
                                                 ' history of end revision.'))
 
672
 
 
673
    # We exit the loop above because we encounter a revision with merges, from
 
674
    # this revision, we need to switch to _graph_view_revisions.
 
675
 
 
676
    # A log including nested merges is required. If the direction is reverse,
 
677
    # we rebase the initial merge depths so that the development line is
 
678
    # shown naturally, i.e. just like it is for linear logging. We can easily
 
679
    # make forward the exact opposite display, but showing the merge revisions
 
680
    # indented at the end seems slightly nicer in that case.
 
681
    view_revisions = itertools.chain(iter(initial_revisions),
 
682
                                     _graph_view_revisions(branch, start_rev_id, end_rev_id,
 
683
                                                           rebase_initial_depths=(
 
684
                                                               direction == 'reverse'),
 
685
                                                           exclude_common_ancestry=exclude_common_ancestry))
 
686
    return view_revisions
 
687
 
 
688
 
 
689
def _has_merges(branch, rev_id):
 
690
    """Does a revision have multiple parents or not?"""
 
691
    parents = branch.repository.get_parent_map([rev_id]).get(rev_id, [])
 
692
    return len(parents) > 1
 
693
 
 
694
 
 
695
def _compute_revno_str(branch, rev_id):
 
696
    """Compute the revno string from a rev_id.
 
697
 
 
698
    :return: The revno string, or None if the revision is not in the supplied
 
699
        branch.
 
700
    """
 
701
    try:
 
702
        revno = branch.revision_id_to_dotted_revno(rev_id)
 
703
    except errors.NoSuchRevision:
 
704
        # The revision must be outside of this branch
 
705
        return None
 
706
    else:
 
707
        return '.'.join(str(n) for n in revno)
 
708
 
 
709
 
 
710
def _is_obvious_ancestor(branch, start_rev_id, end_rev_id):
 
711
    """Is start_rev_id an obvious ancestor of end_rev_id?"""
 
712
    if start_rev_id and end_rev_id:
 
713
        try:
 
714
            start_dotted = branch.revision_id_to_dotted_revno(start_rev_id)
 
715
            end_dotted = branch.revision_id_to_dotted_revno(end_rev_id)
 
716
        except errors.NoSuchRevision:
 
717
            # one or both is not in the branch; not obvious
 
718
            return False
 
719
        if len(start_dotted) == 1 and len(end_dotted) == 1:
 
720
            # both on mainline
 
721
            return start_dotted[0] <= end_dotted[0]
 
722
        elif (len(start_dotted) == 3 and len(end_dotted) == 3 and
 
723
              start_dotted[0:1] == end_dotted[0:1]):
 
724
            # both on same development line
 
725
            return start_dotted[2] <= end_dotted[2]
 
726
        else:
 
727
            # not obvious
 
728
            return False
 
729
    # if either start or end is not specified then we use either the first or
 
730
    # the last revision and *they* are obvious ancestors.
 
731
    return True
 
732
 
 
733
 
 
734
def _linear_view_revisions(branch, start_rev_id, end_rev_id,
 
735
                           exclude_common_ancestry=False):
 
736
    """Calculate a sequence of revisions to view, newest to oldest.
 
737
 
 
738
    :param start_rev_id: the lower revision-id
 
739
    :param end_rev_id: the upper revision-id
 
740
    :param exclude_common_ancestry: Whether the start_rev_id should be part of
 
741
        the iterated revisions.
 
742
    :return: An iterator of (revision_id, dotted_revno, merge_depth) tuples.
 
743
        dotted_revno will be None for ghosts
 
744
    :raises _StartNotLinearAncestor: if a start_rev_id is specified but
 
745
        is not found walking the left-hand history
 
746
    """
 
747
    repo = branch.repository
 
748
    graph = repo.get_graph()
 
749
    if start_rev_id is None and end_rev_id is None:
 
750
        try:
 
751
            br_revno, br_rev_id = branch.last_revision_info()
 
752
        except errors.GhostRevisionsHaveNoRevno:
 
753
            br_rev_id = branch.last_revision()
 
754
            cur_revno = None
 
755
        else:
 
756
            cur_revno = br_revno
 
757
        graph_iter = graph.iter_lefthand_ancestry(br_rev_id,
 
758
                                                  (_mod_revision.NULL_REVISION,))
 
759
        while True:
 
760
            try:
 
761
                revision_id = next(graph_iter)
 
762
            except errors.RevisionNotPresent as e:
 
763
                # Oops, a ghost.
 
764
                yield e.revision_id, None, None
 
765
                break
 
766
            except StopIteration:
 
767
                break
 
768
            else:
 
769
                yield revision_id, str(cur_revno) if cur_revno is not None else None, 0
 
770
                if cur_revno is not None:
 
771
                    cur_revno -= 1
 
772
    else:
 
773
        br_rev_id = branch.last_revision()
 
774
        if end_rev_id is None:
 
775
            end_rev_id = br_rev_id
 
776
        found_start = start_rev_id is None
 
777
        graph_iter = graph.iter_lefthand_ancestry(end_rev_id,
 
778
                                                  (_mod_revision.NULL_REVISION,))
 
779
        while True:
 
780
            try:
 
781
                revision_id = next(graph_iter)
 
782
            except StopIteration:
 
783
                break
 
784
            except errors.RevisionNotPresent as e:
 
785
                # Oops, a ghost.
 
786
                yield e.revision_id, None, None
 
787
                break
 
788
            else:
 
789
                revno_str = _compute_revno_str(branch, revision_id)
 
790
                if not found_start and revision_id == start_rev_id:
 
791
                    if not exclude_common_ancestry:
 
792
                        yield revision_id, revno_str, 0
 
793
                    found_start = True
 
794
                    break
 
795
                else:
 
796
                    yield revision_id, revno_str, 0
 
797
        if not found_start:
 
798
            raise _StartNotLinearAncestor()
 
799
 
 
800
 
 
801
def _graph_view_revisions(branch, start_rev_id, end_rev_id,
 
802
                          rebase_initial_depths=True,
 
803
                          exclude_common_ancestry=False):
 
804
    """Calculate revisions to view including merges, newest to oldest.
 
805
 
 
806
    :param branch: the branch
 
807
    :param start_rev_id: the lower revision-id
 
808
    :param end_rev_id: the upper revision-id
 
809
    :param rebase_initial_depth: should depths be rebased until a mainline
 
810
      revision is found?
 
811
    :return: An iterator of (revision_id, dotted_revno, merge_depth) tuples.
 
812
    """
 
813
    if exclude_common_ancestry:
 
814
        stop_rule = 'with-merges-without-common-ancestry'
 
815
    else:
 
816
        stop_rule = 'with-merges'
 
817
    view_revisions = branch.iter_merge_sorted_revisions(
 
818
        start_revision_id=end_rev_id, stop_revision_id=start_rev_id,
 
819
        stop_rule=stop_rule)
 
820
    if not rebase_initial_depths:
 
821
        for (rev_id, merge_depth, revno, end_of_merge
 
822
             ) in view_revisions:
 
823
            yield rev_id, '.'.join(map(str, revno)), merge_depth
 
824
    else:
 
825
        # We're following a development line starting at a merged revision.
 
826
        # We need to adjust depths down by the initial depth until we find
 
827
        # a depth less than it. Then we use that depth as the adjustment.
 
828
        # If and when we reach the mainline, depth adjustment ends.
 
829
        depth_adjustment = None
 
830
        for (rev_id, merge_depth, revno, end_of_merge
 
831
             ) in view_revisions:
 
832
            if depth_adjustment is None:
 
833
                depth_adjustment = merge_depth
 
834
            if depth_adjustment:
 
835
                if merge_depth < depth_adjustment:
 
836
                    # From now on we reduce the depth adjustement, this can be
 
837
                    # surprising for users. The alternative requires two passes
 
838
                    # which breaks the fast display of the first revision
 
839
                    # though.
 
840
                    depth_adjustment = merge_depth
 
841
                merge_depth -= depth_adjustment
 
842
            yield rev_id, '.'.join(map(str, revno)), merge_depth
 
843
 
 
844
 
 
845
def _rebase_merge_depth(view_revisions):
 
846
    """Adjust depths upwards so the top level is 0."""
 
847
    # If either the first or last revision have a merge_depth of 0, we're done
 
848
    if view_revisions and view_revisions[0][2] and view_revisions[-1][2]:
 
849
        min_depth = min([d for r, n, d in view_revisions])
 
850
        if min_depth != 0:
 
851
            view_revisions = [(r, n, d - min_depth)
 
852
                              for r, n, d in view_revisions]
 
853
    return view_revisions
 
854
 
 
855
 
 
856
def make_log_rev_iterator(branch, view_revisions, generate_delta, search,
 
857
                          file_ids=None, direction='reverse'):
 
858
    """Create a revision iterator for log.
 
859
 
 
860
    :param branch: The branch being logged.
 
861
    :param view_revisions: The revisions being viewed.
 
862
    :param generate_delta: Whether to generate a delta for each revision.
 
863
      Permitted values are None, 'full' and 'partial'.
 
864
    :param search: A user text search string.
 
865
    :param file_ids: If non empty, only revisions matching one or more of
 
866
      the file-ids are to be kept.
 
867
    :param direction: the direction in which view_revisions is sorted
 
868
    :return: An iterator over lists of ((rev_id, revno, merge_depth), rev,
 
869
        delta).
 
870
    """
 
871
    # Convert view_revisions into (view, None, None) groups to fit with
 
872
    # the standard interface here.
 
873
    if isinstance(view_revisions, list):
 
874
        # A single batch conversion is faster than many incremental ones.
 
875
        # As we have all the data, do a batch conversion.
 
876
        nones = [None] * len(view_revisions)
 
877
        log_rev_iterator = iter([list(zip(view_revisions, nones, nones))])
 
878
    else:
 
879
        def _convert():
 
880
            for view in view_revisions:
 
881
                yield (view, None, None)
 
882
        log_rev_iterator = iter([_convert()])
 
883
    for adapter in log_adapters:
 
884
        # It would be nicer if log adapters were first class objects
 
885
        # with custom parameters. This will do for now. IGC 20090127
 
886
        if adapter == _make_delta_filter:
 
887
            log_rev_iterator = adapter(
 
888
                branch, generate_delta, search, log_rev_iterator, file_ids,
 
889
                direction)
 
890
        else:
 
891
            log_rev_iterator = adapter(
 
892
                branch, generate_delta, search, log_rev_iterator)
 
893
    return log_rev_iterator
 
894
 
 
895
 
 
896
def _make_search_filter(branch, generate_delta, match, log_rev_iterator):
 
897
    """Create a filtered iterator of log_rev_iterator matching on a regex.
 
898
 
 
899
    :param branch: The branch being logged.
 
900
    :param generate_delta: Whether to generate a delta for each revision.
 
901
    :param match: A dictionary with properties as keys and lists of strings
 
902
        as values. To match, a revision may match any of the supplied strings
 
903
        within a single property but must match at least one string for each
 
904
        property.
 
905
    :param log_rev_iterator: An input iterator containing all revisions that
 
906
        could be displayed, in lists.
 
907
    :return: An iterator over lists of ((rev_id, revno, merge_depth), rev,
 
908
        delta).
 
909
    """
 
910
    if not match:
 
911
        return log_rev_iterator
 
912
    # Use lazy_compile so mapping to InvalidPattern error occurs.
 
913
    searchRE = [(k, [lazy_regex.lazy_compile(x, re.IGNORECASE) for x in v])
 
914
                for k, v in match.items()]
 
915
    return _filter_re(searchRE, log_rev_iterator)
 
916
 
 
917
 
 
918
def _filter_re(searchRE, log_rev_iterator):
 
919
    for revs in log_rev_iterator:
 
920
        new_revs = [rev for rev in revs if _match_filter(searchRE, rev[1])]
 
921
        if new_revs:
 
922
            yield new_revs
 
923
 
 
924
 
 
925
def _match_filter(searchRE, rev):
 
926
    strings = {
 
927
        'message': (rev.message,),
 
928
        'committer': (rev.committer,),
 
929
        'author': (rev.get_apparent_authors()),
 
930
        'bugs': list(rev.iter_bugs())
 
931
        }
 
932
    strings[''] = [item for inner_list in strings.values()
 
933
                   for item in inner_list]
 
934
    for k, v in searchRE:
 
935
        if k in strings and not _match_any_filter(strings[k], v):
 
936
            return False
 
937
    return True
 
938
 
 
939
 
 
940
def _match_any_filter(strings, res):
 
941
    return any(r.search(s) for r in res for s in strings)
 
942
 
 
943
 
 
944
def _make_delta_filter(branch, generate_delta, search, log_rev_iterator,
 
945
                       fileids=None, direction='reverse'):
 
946
    """Add revision deltas to a log iterator if needed.
 
947
 
 
948
    :param branch: The branch being logged.
 
949
    :param generate_delta: Whether to generate a delta for each revision.
 
950
      Permitted values are None, 'full' and 'partial'.
 
951
    :param search: A user text search string.
 
952
    :param log_rev_iterator: An input iterator containing all revisions that
 
953
        could be displayed, in lists.
 
954
    :param fileids: If non empty, only revisions matching one or more of
 
955
      the file-ids are to be kept.
 
956
    :param direction: the direction in which view_revisions is sorted
 
957
    :return: An iterator over lists of ((rev_id, revno, merge_depth), rev,
 
958
        delta).
 
959
    """
 
960
    if not generate_delta and not fileids:
 
961
        return log_rev_iterator
 
962
    return _generate_deltas(branch.repository, log_rev_iterator,
 
963
                            generate_delta, fileids, direction)
 
964
 
 
965
 
 
966
def _generate_deltas(repository, log_rev_iterator, delta_type, fileids,
 
967
                     direction):
 
968
    """Create deltas for each batch of revisions in log_rev_iterator.
 
969
 
 
970
    If we're only generating deltas for the sake of filtering against
 
971
    file-ids, we stop generating deltas once all file-ids reach the
 
972
    appropriate life-cycle point. If we're receiving data newest to
 
973
    oldest, then that life-cycle point is 'add', otherwise it's 'remove'.
 
974
    """
 
975
    check_fileids = fileids is not None and len(fileids) > 0
 
976
    if check_fileids:
 
977
        fileid_set = set(fileids)
 
978
        if direction == 'reverse':
 
979
            stop_on = 'add'
 
980
        else:
 
981
            stop_on = 'remove'
 
982
    else:
 
983
        fileid_set = None
 
984
    for revs in log_rev_iterator:
 
985
        # If we were matching against fileids and we've run out,
 
986
        # there's nothing left to do
 
987
        if check_fileids and not fileid_set:
 
988
            return
 
989
        revisions = [rev[1] for rev in revs]
 
990
        new_revs = []
 
991
        if delta_type == 'full' and not check_fileids:
 
992
            deltas = repository.get_deltas_for_revisions(revisions)
 
993
            for rev, delta in zip(revs, deltas):
 
994
                new_revs.append((rev[0], rev[1], delta))
 
995
        else:
 
996
            deltas = repository.get_deltas_for_revisions(revisions, fileid_set)
 
997
            for rev, delta in zip(revs, deltas):
 
998
                if check_fileids:
 
999
                    if delta is None or not delta.has_changed():
 
1000
                        continue
 
1001
                    else:
 
1002
                        _update_fileids(delta, fileid_set, stop_on)
 
1003
                        if delta_type is None:
 
1004
                            delta = None
 
1005
                        elif delta_type == 'full':
 
1006
                            # If the file matches all the time, rebuilding
 
1007
                            # a full delta like this in addition to a partial
 
1008
                            # one could be slow. However, it's likely that
 
1009
                            # most revisions won't get this far, making it
 
1010
                            # faster to filter on the partial deltas and
 
1011
                            # build the occasional full delta than always
 
1012
                            # building full deltas and filtering those.
 
1013
                            rev_id = rev[0][0]
 
1014
                            delta = repository.get_revision_delta(rev_id)
 
1015
                new_revs.append((rev[0], rev[1], delta))
 
1016
        yield new_revs
 
1017
 
 
1018
 
 
1019
def _update_fileids(delta, fileids, stop_on):
 
1020
    """Update the set of file-ids to search based on file lifecycle events.
 
1021
 
 
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
 
1025
    """
 
1026
    if stop_on == 'add':
 
1027
        for item in delta.added:
 
1028
            if item[1] in fileids:
 
1029
                fileids.remove(item[1])
 
1030
    elif stop_on == 'delete':
 
1031
        for item in delta.removed:
 
1032
            if item[1] in fileids:
 
1033
                fileids.remove(item[1])
 
1034
 
 
1035
 
 
1036
def _make_revision_objects(branch, generate_delta, search, log_rev_iterator):
 
1037
    """Extract revision objects from the repository
 
1038
 
 
1039
    :param branch: The branch being logged.
 
1040
    :param generate_delta: Whether to generate a delta for each revision.
 
1041
    :param search: A user text search string.
 
1042
    :param log_rev_iterator: An input iterator containing all revisions that
 
1043
        could be displayed, in lists.
 
1044
    :return: An iterator over lists of ((rev_id, revno, merge_depth), rev,
 
1045
        delta).
 
1046
    """
 
1047
    repository = branch.repository
 
1048
    for revs in log_rev_iterator:
 
1049
        # r = revision_id, n = revno, d = merge depth
 
1050
        revision_ids = [view[0] for view, _, _ in revs]
 
1051
        revisions = dict(repository.iter_revisions(revision_ids))
 
1052
        yield [(rev[0], revisions[rev[0][0]], rev[2]) for rev in revs]
 
1053
 
 
1054
 
 
1055
def _make_batch_filter(branch, generate_delta, search, log_rev_iterator):
 
1056
    """Group up a single large batch into smaller ones.
 
1057
 
 
1058
    :param branch: The branch being logged.
 
1059
    :param generate_delta: Whether to generate a delta for each revision.
 
1060
    :param search: A user text search string.
 
1061
    :param log_rev_iterator: An input iterator containing all revisions that
 
1062
        could be displayed, in lists.
 
1063
    :return: An iterator over lists of ((rev_id, revno, merge_depth), rev,
 
1064
        delta).
 
1065
    """
 
1066
    num = 9
 
1067
    for batch in log_rev_iterator:
 
1068
        batch = iter(batch)
 
1069
        while True:
 
1070
            step = [detail for _, detail in zip(range(num), batch)]
 
1071
            if len(step) == 0:
 
1072
                break
 
1073
            yield step
 
1074
            num = min(int(num * 1.5), 200)
 
1075
 
 
1076
 
 
1077
def _get_revision_limits(branch, start_revision, end_revision):
 
1078
    """Get and check revision limits.
 
1079
 
 
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.
 
1088
 
 
1089
    :return: (start_rev_id, end_rev_id) tuple.
 
1090
    """
 
1091
    start_rev_id = None
 
1092
    start_revno = None
 
1093
    if start_revision is not None:
 
1094
        if not isinstance(start_revision, revisionspec.RevisionInfo):
 
1095
            raise TypeError(start_revision)
 
1096
        start_rev_id = start_revision.rev_id
 
1097
        start_revno = start_revision.revno
 
1098
    if start_revno is None:
 
1099
        start_revno = 1
 
1100
 
 
1101
    end_rev_id = None
 
1102
    end_revno = None
 
1103
    if end_revision is not None:
 
1104
        if not isinstance(end_revision, revisionspec.RevisionInfo):
 
1105
            raise TypeError(start_revision)
 
1106
        end_rev_id = end_revision.rev_id
 
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
 
1113
 
 
1114
    if branch.last_revision() != _mod_revision.NULL_REVISION:
 
1115
        if (start_rev_id == _mod_revision.NULL_REVISION
 
1116
                or end_rev_id == _mod_revision.NULL_REVISION):
 
1117
            raise errors.BzrCommandError(
 
1118
                gettext('Logging revision 0 is invalid.'))
 
1119
        if end_revno is not None and start_revno > end_revno:
 
1120
            raise errors.BzrCommandError(
 
1121
                gettext("Start revision must be older than the end revision."))
 
1122
    return (start_rev_id, end_rev_id)
 
1123
 
 
1124
 
 
1125
def _get_mainline_revs(branch, start_revision, end_revision):
 
1126
    """Get the mainline revisions from the branch.
 
1127
 
 
1128
    Generates the list of mainline revisions for the branch.
 
1129
 
 
1130
    :param  branch: The branch containing the revisions.
 
1131
 
 
1132
    :param  start_revision: The first revision to be logged.
 
1133
            For backwards compatibility this may be a mainline integer revno,
 
1134
            but for merge revision support a RevisionInfo is expected.
 
1135
 
 
1136
    :param  end_revision: The last revision to be logged.
 
1137
            For backwards compatibility this may be a mainline integer revno,
 
1138
            but for merge revision support a RevisionInfo is expected.
 
1139
 
 
1140
    :return: A (mainline_revs, rev_nos, start_rev_id, end_rev_id) tuple.
 
1141
    """
 
1142
    branch_revno, branch_last_revision = branch.last_revision_info()
 
1143
    if branch_revno == 0:
 
1144
        return None, None, None, None
 
1145
 
 
1146
    # For mainline generation, map start_revision and end_revision to
 
1147
    # mainline revnos. If the revision is not on the mainline choose the
 
1148
    # appropriate extreme of the mainline instead - the extra will be
 
1149
    # filtered later.
 
1150
    # Also map the revisions to rev_ids, to be used in the later filtering
 
1151
    # stage.
 
1152
    start_rev_id = None
 
1153
    if start_revision is None:
 
1154
        start_revno = 1
 
1155
    else:
 
1156
        if isinstance(start_revision, revisionspec.RevisionInfo):
 
1157
            start_rev_id = start_revision.rev_id
 
1158
            start_revno = start_revision.revno or 1
 
1159
        else:
 
1160
            branch.check_real_revno(start_revision)
 
1161
            start_revno = start_revision
 
1162
 
 
1163
    end_rev_id = None
 
1164
    if end_revision is None:
 
1165
        end_revno = branch_revno
 
1166
    else:
 
1167
        if isinstance(end_revision, revisionspec.RevisionInfo):
 
1168
            end_rev_id = end_revision.rev_id
 
1169
            end_revno = end_revision.revno or branch_revno
 
1170
        else:
 
1171
            branch.check_real_revno(end_revision)
 
1172
            end_revno = end_revision
 
1173
 
 
1174
    if ((start_rev_id == _mod_revision.NULL_REVISION)
 
1175
            or (end_rev_id == _mod_revision.NULL_REVISION)):
 
1176
        raise errors.BzrCommandError(gettext('Logging revision 0 is invalid.'))
 
1177
    if start_revno > end_revno:
 
1178
        raise errors.BzrCommandError(gettext("Start revision must be older "
 
1179
                                             "than the end revision."))
 
1180
 
 
1181
    if end_revno < start_revno:
 
1182
        return None, None, None, None
 
1183
    cur_revno = branch_revno
 
1184
    rev_nos = {}
 
1185
    mainline_revs = []
 
1186
    graph = branch.repository.get_graph()
 
1187
    for revision_id in graph.iter_lefthand_ancestry(
 
1188
            branch_last_revision, (_mod_revision.NULL_REVISION,)):
 
1189
        if cur_revno < start_revno:
 
1190
            # We have gone far enough, but we always add 1 more revision
 
1191
            rev_nos[revision_id] = cur_revno
 
1192
            mainline_revs.append(revision_id)
 
1193
            break
 
1194
        if cur_revno <= end_revno:
 
1195
            rev_nos[revision_id] = cur_revno
 
1196
            mainline_revs.append(revision_id)
 
1197
        cur_revno -= 1
 
1198
    else:
 
1199
        # We walked off the edge of all revisions, so we add a 'None' marker
 
1200
        mainline_revs.append(None)
 
1201
 
 
1202
    mainline_revs.reverse()
 
1203
 
 
1204
    # override the mainline to look like the revision history.
 
1205
    return mainline_revs, rev_nos, start_rev_id, end_rev_id
 
1206
 
 
1207
 
 
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.
 
1211
 
 
1212
    The function filters view_revisions and returns a subset.
 
1213
    This includes the revisions which directly change the file id,
 
1214
    and the revisions which merge these changes. So if the
 
1215
    revision graph is::
 
1216
 
 
1217
        A-.
 
1218
        |\ \
 
1219
        B C E
 
1220
        |/ /
 
1221
        D |
 
1222
        |\|
 
1223
        | F
 
1224
        |/
 
1225
        G
 
1226
 
 
1227
    And 'C' changes a file, then both C and D will be returned. F will not be
 
1228
    returned even though it brings the changes to C into the branch starting
 
1229
    with E. (Note that if we were using F as the tip instead of G, then we
 
1230
    would see C, D, F.)
 
1231
 
 
1232
    This will also be restricted based on a subset of the mainline.
 
1233
 
 
1234
    :param branch: The branch where we can get text revision information.
 
1235
 
 
1236
    :param file_id: Filter out revisions that do not touch file_id.
 
1237
 
 
1238
    :param view_revisions: A list of (revision_id, dotted_revno, merge_depth)
 
1239
        tuples. This is the list of revisions which will be filtered. It is
 
1240
        assumed that view_revisions is in merge_sort order (i.e. newest
 
1241
        revision first ).
 
1242
 
 
1243
    :param include_merges: include merge revisions in the result or not
 
1244
 
 
1245
    :return: A list of (revision_id, dotted_revno, merge_depth) tuples.
 
1246
    """
 
1247
    # Lookup all possible text keys to determine which ones actually modified
 
1248
    # the file.
 
1249
    graph = branch.repository.get_file_graph()
 
1250
    get_parent_map = graph.get_parent_map
 
1251
    text_keys = [(file_id, rev_id) for rev_id, revno, depth in view_revisions]
 
1252
    next_keys = None
 
1253
    # Looking up keys in batches of 1000 can cut the time in half, as well as
 
1254
    # memory consumption. GraphIndex *does* like to look for a few keys in
 
1255
    # parallel, it just doesn't like looking for *lots* of keys in parallel.
 
1256
    # TODO: This code needs to be re-evaluated periodically as we tune the
 
1257
    #       indexing layer. We might consider passing in hints as to the known
 
1258
    #       access pattern (sparse/clustered, high success rate/low success
 
1259
    #       rate). This particular access is clustered with a low success rate.
 
1260
    modified_text_revisions = set()
 
1261
    chunk_size = 1000
 
1262
    for start in range(0, len(text_keys), chunk_size):
 
1263
        next_keys = text_keys[start:start + chunk_size]
 
1264
        # Only keep the revision_id portion of the key
 
1265
        modified_text_revisions.update(
 
1266
            [k[1] for k in get_parent_map(next_keys)])
 
1267
    del text_keys, next_keys
 
1268
 
 
1269
    result = []
 
1270
    # Track what revisions will merge the current revision, replace entries
 
1271
    # with 'None' when they have been added to result
 
1272
    current_merge_stack = [None]
 
1273
    for info in view_revisions:
 
1274
        rev_id, revno, depth = info
 
1275
        if depth == len(current_merge_stack):
 
1276
            current_merge_stack.append(info)
 
1277
        else:
 
1278
            del current_merge_stack[depth + 1:]
 
1279
            current_merge_stack[-1] = info
 
1280
 
 
1281
        if rev_id in modified_text_revisions:
 
1282
            # This needs to be logged, along with the extra revisions
 
1283
            for idx in range(len(current_merge_stack)):
 
1284
                node = current_merge_stack[idx]
 
1285
                if node is not None:
 
1286
                    if include_merges or node[2] == 0:
 
1287
                        result.append(node)
 
1288
                        current_merge_stack[idx] = None
 
1289
    return result
 
1290
 
 
1291
 
 
1292
def reverse_by_depth(merge_sorted_revisions, _depth=0):
 
1293
    """Reverse revisions by depth.
 
1294
 
 
1295
    Revisions with a different depth are sorted as a group with the previous
 
1296
    revision of that depth.  There may be no topological justification for this
 
1297
    but it looks much nicer.
 
1298
    """
 
1299
    # Add a fake revision at start so that we can always attach sub revisions
 
1300
    merge_sorted_revisions = [(None, None, _depth)] + merge_sorted_revisions
 
1301
    zd_revisions = []
 
1302
    for val in merge_sorted_revisions:
 
1303
        if val[2] == _depth:
 
1304
            # Each revision at the current depth becomes a chunk grouping all
 
1305
            # higher depth revisions.
 
1306
            zd_revisions.append([val])
 
1307
        else:
 
1308
            zd_revisions[-1].append(val)
 
1309
    for revisions in zd_revisions:
 
1310
        if len(revisions) > 1:
 
1311
            # We have higher depth revisions, let reverse them locally
 
1312
            revisions[1:] = reverse_by_depth(revisions[1:], _depth + 1)
 
1313
    zd_revisions.reverse()
 
1314
    result = []
 
1315
    for chunk in zd_revisions:
 
1316
        result.extend(chunk)
 
1317
    if _depth == 0:
 
1318
        # Top level call, get rid of the fake revisions that have been added
 
1319
        result = [r for r in result if r[0] is not None and r[1] is not None]
 
1320
    return result
 
1321
 
 
1322
 
 
1323
class LogRevision(object):
 
1324
    """A revision to be logged (by LogFormatter.log_revision).
 
1325
 
 
1326
    A simple wrapper for the attributes of a revision to be logged.
 
1327
    The attributes may or may not be populated, as determined by the
 
1328
    logging options and the log formatter capabilities.
 
1329
    """
 
1330
 
 
1331
    def __init__(self, rev=None, revno=None, merge_depth=0, delta=None,
 
1332
                 tags=None, diff=None, signature=None):
 
1333
        self.rev = rev
 
1334
        if revno is None:
 
1335
            self.revno = None
 
1336
        else:
 
1337
            self.revno = str(revno)
 
1338
        self.merge_depth = merge_depth
 
1339
        self.delta = delta
 
1340
        self.tags = tags
 
1341
        self.diff = diff
 
1342
        self.signature = signature
 
1343
 
 
1344
 
 
1345
class LogFormatter(object):
 
1346
    """Abstract class to display log messages.
 
1347
 
 
1348
    At a minimum, a derived class must implement the log_revision method.
 
1349
 
 
1350
    If the LogFormatter needs to be informed of the beginning or end of
 
1351
    a log it should implement the begin_log and/or end_log hook methods.
 
1352
 
 
1353
    A LogFormatter should define the following supports_XXX flags
 
1354
    to indicate which LogRevision attributes it supports:
 
1355
 
 
1356
    - supports_delta must be True if this log formatter supports delta.
 
1357
      Otherwise the delta attribute may not be populated.  The 'delta_format'
 
1358
      attribute describes whether the 'short_status' format (1) or the long
 
1359
      one (2) should be used.
 
1360
 
 
1361
    - supports_merge_revisions must be True if this log formatter supports
 
1362
      merge revisions.  If not, then only mainline revisions will be passed
 
1363
      to the formatter.
 
1364
 
 
1365
    - preferred_levels is the number of levels this formatter defaults to.
 
1366
      The default value is zero meaning display all levels.
 
1367
      This value is only relevant if supports_merge_revisions is True.
 
1368
 
 
1369
    - supports_tags must be True if this log formatter supports tags.
 
1370
      Otherwise the tags attribute may not be populated.
 
1371
 
 
1372
    - supports_diff must be True if this log formatter supports diffs.
 
1373
      Otherwise the diff attribute may not be populated.
 
1374
 
 
1375
    - supports_signatures must be True if this log formatter supports GPG
 
1376
      signatures.
 
1377
 
 
1378
    Plugins can register functions to show custom revision properties using
 
1379
    the properties_handler_registry. The registered function
 
1380
    must respect the following interface description::
 
1381
 
 
1382
        def my_show_properties(properties_dict):
 
1383
            # code that returns a dict {'name':'value'} of the properties
 
1384
            # to be shown
 
1385
    """
 
1386
    preferred_levels = 0
 
1387
 
 
1388
    def __init__(self, to_file, show_ids=False, show_timezone='original',
 
1389
                 delta_format=None, levels=None, show_advice=False,
 
1390
                 to_exact_file=None, author_list_handler=None):
 
1391
        """Create a LogFormatter.
 
1392
 
 
1393
        :param to_file: the file to output to
 
1394
        :param to_exact_file: if set, gives an output stream to which
 
1395
             non-Unicode diffs are written.
 
1396
        :param show_ids: if True, revision-ids are to be displayed
 
1397
        :param show_timezone: the timezone to use
 
1398
        :param delta_format: the level of delta information to display
 
1399
          or None to leave it to the formatter to decide
 
1400
        :param levels: the number of levels to display; None or -1 to
 
1401
          let the log formatter decide.
 
1402
        :param show_advice: whether to show advice at the end of the
 
1403
          log or not
 
1404
        :param author_list_handler: callable generating a list of
 
1405
          authors to display for a given revision
 
1406
        """
 
1407
        self.to_file = to_file
 
1408
        # 'exact' stream used to show diff, it should print content 'as is'
 
1409
        # and should not try to decode/encode it to unicode to avoid bug
 
1410
        # #328007
 
1411
        if to_exact_file is not None:
 
1412
            self.to_exact_file = to_exact_file
 
1413
        else:
 
1414
            # XXX: somewhat hacky; this assumes it's a codec writer; it's
 
1415
            # better for code that expects to get diffs to pass in the exact
 
1416
            # file stream
 
1417
            self.to_exact_file = getattr(to_file, 'stream', to_file)
 
1418
        self.show_ids = show_ids
 
1419
        self.show_timezone = show_timezone
 
1420
        if delta_format is None:
 
1421
            # Ensures backward compatibility
 
1422
            delta_format = 2  # long format
 
1423
        self.delta_format = delta_format
 
1424
        self.levels = levels
 
1425
        self._show_advice = show_advice
 
1426
        self._merge_count = 0
 
1427
        self._author_list_handler = author_list_handler
 
1428
 
 
1429
    def get_levels(self):
 
1430
        """Get the number of levels to display or 0 for all."""
 
1431
        if getattr(self, 'supports_merge_revisions', False):
 
1432
            if self.levels is None or self.levels == -1:
 
1433
                self.levels = self.preferred_levels
 
1434
        else:
 
1435
            self.levels = 1
 
1436
        return self.levels
 
1437
 
 
1438
    def log_revision(self, revision):
 
1439
        """Log a revision.
 
1440
 
 
1441
        :param  revision:   The LogRevision to be logged.
 
1442
        """
 
1443
        raise NotImplementedError('not implemented in abstract base')
 
1444
 
 
1445
    def show_advice(self):
 
1446
        """Output user advice, if any, when the log is completed."""
 
1447
        if self._show_advice and self.levels == 1 and self._merge_count > 0:
 
1448
            advice_sep = self.get_advice_separator()
 
1449
            if advice_sep:
 
1450
                self.to_file.write(advice_sep)
 
1451
            self.to_file.write(
 
1452
                "Use --include-merged or -n0 to see merged revisions.\n")
 
1453
 
 
1454
    def get_advice_separator(self):
 
1455
        """Get the text separating the log from the closing advice."""
 
1456
        return ''
 
1457
 
 
1458
    def short_committer(self, rev):
 
1459
        name, address = config.parse_username(rev.committer)
 
1460
        if name:
 
1461
            return name
 
1462
        return address
 
1463
 
 
1464
    def short_author(self, rev):
 
1465
        return self.authors(rev, 'first', short=True, sep=', ')
 
1466
 
 
1467
    def authors(self, rev, who, short=False, sep=None):
 
1468
        """Generate list of authors, taking --authors option into account.
 
1469
 
 
1470
        The caller has to specify the name of a author list handler,
 
1471
        as provided by the author list registry, using the ``who``
 
1472
        argument.  That name only sets a default, though: when the
 
1473
        user selected a different author list generation using the
 
1474
        ``--authors`` command line switch, as represented by the
 
1475
        ``author_list_handler`` constructor argument, that value takes
 
1476
        precedence.
 
1477
 
 
1478
        :param rev: The revision for which to generate the list of authors.
 
1479
        :param who: Name of the default handler.
 
1480
        :param short: Whether to shorten names to either name or address.
 
1481
        :param sep: What separator to use for automatic concatenation.
 
1482
        """
 
1483
        if self._author_list_handler is not None:
 
1484
            # The user did specify --authors, which overrides the default
 
1485
            author_list_handler = self._author_list_handler
 
1486
        else:
 
1487
            # The user didn't specify --authors, so we use the caller's default
 
1488
            author_list_handler = author_list_registry.get(who)
 
1489
        names = author_list_handler(rev)
 
1490
        if short:
 
1491
            for i in range(len(names)):
 
1492
                name, address = config.parse_username(names[i])
 
1493
                if name:
 
1494
                    names[i] = name
 
1495
                else:
 
1496
                    names[i] = address
 
1497
        if sep is not None:
 
1498
            names = sep.join(names)
 
1499
        return names
 
1500
 
 
1501
    def merge_marker(self, revision):
 
1502
        """Get the merge marker to include in the output or '' if none."""
 
1503
        if len(revision.rev.parent_ids) > 1:
 
1504
            self._merge_count += 1
 
1505
            return ' [merge]'
 
1506
        else:
 
1507
            return ''
 
1508
 
 
1509
    def show_properties(self, revision, indent):
 
1510
        """Displays the custom properties returned by each registered handler.
 
1511
 
 
1512
        If a registered handler raises an error it is propagated.
 
1513
        """
 
1514
        for line in self.custom_properties(revision):
 
1515
            self.to_file.write("%s%s\n" % (indent, line))
 
1516
 
 
1517
    def custom_properties(self, revision):
 
1518
        """Format the custom properties returned by each registered handler.
 
1519
 
 
1520
        If a registered handler raises an error it is propagated.
 
1521
 
 
1522
        :return: a list of formatted lines (excluding trailing newlines)
 
1523
        """
 
1524
        lines = self._foreign_info_properties(revision)
 
1525
        for key, handler in properties_handler_registry.iteritems():
 
1526
            try:
 
1527
                lines.extend(self._format_properties(handler(revision)))
 
1528
            except Exception:
 
1529
                trace.log_exception_quietly()
 
1530
                trace.print_exception(sys.exc_info(), self.to_file)
 
1531
        return lines
 
1532
 
 
1533
    def _foreign_info_properties(self, rev):
 
1534
        """Custom log displayer for foreign revision identifiers.
 
1535
 
 
1536
        :param rev: Revision object.
 
1537
        """
 
1538
        # Revision comes directly from a foreign repository
 
1539
        if isinstance(rev, foreign.ForeignRevision):
 
1540
            return self._format_properties(
 
1541
                rev.mapping.vcs.show_foreign_revid(rev.foreign_revid))
 
1542
 
 
1543
        # Imported foreign revision revision ids always contain :
 
1544
        if b":" not in rev.revision_id:
 
1545
            return []
 
1546
 
 
1547
        # Revision was once imported from a foreign repository
 
1548
        try:
 
1549
            foreign_revid, mapping = \
 
1550
                foreign.foreign_vcs_registry.parse_revision_id(rev.revision_id)
 
1551
        except errors.InvalidRevisionId:
 
1552
            return []
 
1553
 
 
1554
        return self._format_properties(
 
1555
            mapping.vcs.show_foreign_revid(foreign_revid))
 
1556
 
 
1557
    def _format_properties(self, properties):
 
1558
        lines = []
 
1559
        for key, value in properties.items():
 
1560
            lines.append(key + ': ' + value)
 
1561
        return lines
 
1562
 
 
1563
    def show_diff(self, to_file, diff, indent):
 
1564
        encoding = get_terminal_encoding()
 
1565
        for l in diff.rstrip().split(b'\n'):
 
1566
            to_file.write(indent + l.decode(encoding, 'ignore') + '\n')
 
1567
 
 
1568
 
 
1569
# Separator between revisions in long format
 
1570
_LONG_SEP = '-' * 60
 
1571
 
 
1572
 
 
1573
class LongLogFormatter(LogFormatter):
 
1574
 
 
1575
    supports_merge_revisions = True
 
1576
    preferred_levels = 1
 
1577
    supports_delta = True
 
1578
    supports_tags = True
 
1579
    supports_diff = True
 
1580
    supports_signatures = True
 
1581
 
 
1582
    def __init__(self, *args, **kwargs):
 
1583
        super(LongLogFormatter, self).__init__(*args, **kwargs)
 
1584
        if self.show_timezone == 'original':
 
1585
            self.date_string = self._date_string_original_timezone
 
1586
        else:
 
1587
            self.date_string = self._date_string_with_timezone
 
1588
 
 
1589
    def _date_string_with_timezone(self, rev):
 
1590
        return format_date(rev.timestamp, rev.timezone or 0,
 
1591
                           self.show_timezone)
 
1592
 
 
1593
    def _date_string_original_timezone(self, rev):
 
1594
        return format_date_with_offset_in_original_timezone(rev.timestamp,
 
1595
                                                            rev.timezone or 0)
 
1596
 
 
1597
    def log_revision(self, revision):
 
1598
        """Log a revision, either merged or not."""
 
1599
        indent = '    ' * revision.merge_depth
 
1600
        lines = [_LONG_SEP]
 
1601
        if revision.revno is not None:
 
1602
            lines.append('revno: %s%s' % (revision.revno,
 
1603
                                          self.merge_marker(revision)))
 
1604
        if revision.tags:
 
1605
            lines.append('tags: %s' % (', '.join(sorted(revision.tags))))
 
1606
        if self.show_ids or revision.revno is None:
 
1607
            lines.append('revision-id: %s' %
 
1608
                         (revision.rev.revision_id.decode('utf-8'),))
 
1609
        if self.show_ids:
 
1610
            for parent_id in revision.rev.parent_ids:
 
1611
                lines.append('parent: %s' % (parent_id.decode('utf-8'),))
 
1612
        lines.extend(self.custom_properties(revision.rev))
 
1613
 
 
1614
        committer = revision.rev.committer
 
1615
        authors = self.authors(revision.rev, 'all')
 
1616
        if authors != [committer]:
 
1617
            lines.append('author: %s' % (", ".join(authors),))
 
1618
        lines.append('committer: %s' % (committer,))
 
1619
 
 
1620
        branch_nick = revision.rev.properties.get('branch-nick', None)
 
1621
        if branch_nick is not None:
 
1622
            lines.append('branch nick: %s' % (branch_nick,))
 
1623
 
 
1624
        lines.append('timestamp: %s' % (self.date_string(revision.rev),))
 
1625
 
 
1626
        if revision.signature is not None:
 
1627
            lines.append('signature: ' + revision.signature)
 
1628
 
 
1629
        lines.append('message:')
 
1630
        if not revision.rev.message:
 
1631
            lines.append('  (no message)')
 
1632
        else:
 
1633
            message = revision.rev.message.rstrip('\r\n')
 
1634
            for l in message.split('\n'):
 
1635
                lines.append('  %s' % (l,))
 
1636
 
 
1637
        # Dump the output, appending the delta and diff if requested
 
1638
        to_file = self.to_file
 
1639
        to_file.write("%s%s\n" % (indent, ('\n' + indent).join(lines)))
 
1640
        if revision.delta is not None:
 
1641
            # Use the standard status output to display changes
 
1642
            from breezy.delta import report_delta
 
1643
            report_delta(to_file, revision.delta, short_status=False,
 
1644
                         show_ids=self.show_ids, indent=indent)
 
1645
        if revision.diff is not None:
 
1646
            to_file.write(indent + 'diff:\n')
 
1647
            to_file.flush()
 
1648
            # Note: we explicitly don't indent the diff (relative to the
 
1649
            # revision information) so that the output can be fed to patch -p0
 
1650
            self.show_diff(self.to_exact_file, revision.diff, indent)
 
1651
            self.to_exact_file.flush()
 
1652
 
 
1653
    def get_advice_separator(self):
 
1654
        """Get the text separating the log from the closing advice."""
 
1655
        return '-' * 60 + '\n'
 
1656
 
 
1657
 
 
1658
class ShortLogFormatter(LogFormatter):
 
1659
 
 
1660
    supports_merge_revisions = True
 
1661
    preferred_levels = 1
 
1662
    supports_delta = True
 
1663
    supports_tags = True
 
1664
    supports_diff = True
 
1665
 
 
1666
    def __init__(self, *args, **kwargs):
 
1667
        super(ShortLogFormatter, self).__init__(*args, **kwargs)
 
1668
        self.revno_width_by_depth = {}
 
1669
 
 
1670
    def log_revision(self, revision):
 
1671
        # We need two indents: one per depth and one for the information
 
1672
        # relative to that indent. Most mainline revnos are 5 chars or
 
1673
        # less while dotted revnos are typically 11 chars or less. Once
 
1674
        # calculated, we need to remember the offset for a given depth
 
1675
        # as we might be starting from a dotted revno in the first column
 
1676
        # and we want subsequent mainline revisions to line up.
 
1677
        depth = revision.merge_depth
 
1678
        indent = '    ' * depth
 
1679
        revno_width = self.revno_width_by_depth.get(depth)
 
1680
        if revno_width is None:
 
1681
            if revision.revno is None or revision.revno.find('.') == -1:
 
1682
                # mainline revno, e.g. 12345
 
1683
                revno_width = 5
 
1684
            else:
 
1685
                # dotted revno, e.g. 12345.10.55
 
1686
                revno_width = 11
 
1687
            self.revno_width_by_depth[depth] = revno_width
 
1688
        offset = ' ' * (revno_width + 1)
 
1689
 
 
1690
        to_file = self.to_file
 
1691
        tags = ''
 
1692
        if revision.tags:
 
1693
            tags = ' {%s}' % (', '.join(sorted(revision.tags)))
 
1694
        to_file.write(indent + "%*s %s\t%s%s%s\n" % (revno_width,
 
1695
                                                     revision.revno or "", self.short_author(
 
1696
                                                         revision.rev),
 
1697
                                                     format_date(revision.rev.timestamp,
 
1698
                                                                 revision.rev.timezone or 0,
 
1699
                                                                 self.show_timezone, date_fmt="%Y-%m-%d",
 
1700
                                                                 show_offset=False),
 
1701
                                                     tags, self.merge_marker(revision)))
 
1702
        self.show_properties(revision.rev, indent + offset)
 
1703
        if self.show_ids or revision.revno is None:
 
1704
            to_file.write(indent + offset + 'revision-id:%s\n'
 
1705
                          % (revision.rev.revision_id.decode('utf-8'),))
 
1706
        if not revision.rev.message:
 
1707
            to_file.write(indent + offset + '(no message)\n')
 
1708
        else:
 
1709
            message = revision.rev.message.rstrip('\r\n')
 
1710
            for l in message.split('\n'):
 
1711
                to_file.write(indent + offset + '%s\n' % (l,))
 
1712
 
 
1713
        if revision.delta is not None:
 
1714
            # Use the standard status output to display changes
 
1715
            from breezy.delta import report_delta
 
1716
            report_delta(to_file, revision.delta,
 
1717
                         short_status=self.delta_format == 1,
 
1718
                         show_ids=self.show_ids, indent=indent + offset)
 
1719
        if revision.diff is not None:
 
1720
            self.show_diff(self.to_exact_file, revision.diff, '      ')
 
1721
        to_file.write('\n')
 
1722
 
 
1723
 
 
1724
class LineLogFormatter(LogFormatter):
 
1725
 
 
1726
    supports_merge_revisions = True
 
1727
    preferred_levels = 1
 
1728
    supports_tags = True
 
1729
 
 
1730
    def __init__(self, *args, **kwargs):
 
1731
        super(LineLogFormatter, self).__init__(*args, **kwargs)
 
1732
        width = terminal_width()
 
1733
        if width is not None:
 
1734
            # we need one extra space for terminals that wrap on last char
 
1735
            width = width - 1
 
1736
        self._max_chars = width
 
1737
 
 
1738
    def truncate(self, str, max_len):
 
1739
        if max_len is None or len(str) <= max_len:
 
1740
            return str
 
1741
        return str[:max_len - 3] + '...'
 
1742
 
 
1743
    def date_string(self, rev):
 
1744
        return format_date(rev.timestamp, rev.timezone or 0,
 
1745
                           self.show_timezone, date_fmt="%Y-%m-%d",
 
1746
                           show_offset=False)
 
1747
 
 
1748
    def message(self, rev):
 
1749
        if not rev.message:
 
1750
            return '(no message)'
 
1751
        else:
 
1752
            return rev.message
 
1753
 
 
1754
    def log_revision(self, revision):
 
1755
        indent = '  ' * revision.merge_depth
 
1756
        self.to_file.write(self.log_string(revision.revno, revision.rev,
 
1757
                                           self._max_chars, revision.tags, indent))
 
1758
        self.to_file.write('\n')
 
1759
 
 
1760
    def log_string(self, revno, rev, max_chars, tags=None, prefix=''):
 
1761
        """Format log info into one string. Truncate tail of string
 
1762
 
 
1763
        :param revno:      revision number or None.
 
1764
                           Revision numbers counts from 1.
 
1765
        :param rev:        revision object
 
1766
        :param max_chars:  maximum length of resulting string
 
1767
        :param tags:       list of tags or None
 
1768
        :param prefix:     string to prefix each line
 
1769
        :return:           formatted truncated string
 
1770
        """
 
1771
        out = []
 
1772
        if revno:
 
1773
            # show revno only when is not None
 
1774
            out.append("%s:" % revno)
 
1775
        if max_chars is not None:
 
1776
            out.append(self.truncate(
 
1777
                self.short_author(rev), (max_chars + 3) // 4))
 
1778
        else:
 
1779
            out.append(self.short_author(rev))
 
1780
        out.append(self.date_string(rev))
 
1781
        if len(rev.parent_ids) > 1:
 
1782
            out.append('[merge]')
 
1783
        if tags:
 
1784
            tag_str = '{%s}' % (', '.join(sorted(tags)))
 
1785
            out.append(tag_str)
 
1786
        out.append(rev.get_summary())
 
1787
        return self.truncate(prefix + " ".join(out).rstrip('\n'), max_chars)
 
1788
 
 
1789
 
 
1790
class GnuChangelogLogFormatter(LogFormatter):
 
1791
 
 
1792
    supports_merge_revisions = True
 
1793
    supports_delta = True
 
1794
 
 
1795
    def log_revision(self, revision):
 
1796
        """Log a revision, either merged or not."""
 
1797
        to_file = self.to_file
 
1798
 
 
1799
        date_str = format_date(revision.rev.timestamp,
 
1800
                               revision.rev.timezone or 0,
 
1801
                               self.show_timezone,
 
1802
                               date_fmt='%Y-%m-%d',
 
1803
                               show_offset=False)
 
1804
        committer_str = self.authors(revision.rev, 'first', sep=', ')
 
1805
        committer_str = committer_str.replace(' <', '  <')
 
1806
        to_file.write('%s  %s\n\n' % (date_str, committer_str))
 
1807
 
 
1808
        if revision.delta is not None and revision.delta.has_changed():
 
1809
            for c in revision.delta.added + revision.delta.removed + revision.delta.modified:
 
1810
                path, = c[:1]
 
1811
                to_file.write('\t* %s:\n' % (path,))
 
1812
            for c in revision.delta.renamed:
 
1813
                oldpath, newpath = c[:2]
 
1814
                # For renamed files, show both the old and the new path
 
1815
                to_file.write('\t* %s:\n\t* %s:\n' % (oldpath, newpath))
 
1816
            to_file.write('\n')
 
1817
 
 
1818
        if not revision.rev.message:
 
1819
            to_file.write('\tNo commit message\n')
 
1820
        else:
 
1821
            message = revision.rev.message.rstrip('\r\n')
 
1822
            for l in message.split('\n'):
 
1823
                to_file.write('\t%s\n' % (l.lstrip(),))
 
1824
            to_file.write('\n')
 
1825
 
 
1826
 
 
1827
def line_log(rev, max_chars):
 
1828
    lf = LineLogFormatter(None)
 
1829
    return lf.log_string(None, rev, max_chars)
 
1830
 
 
1831
 
 
1832
class LogFormatterRegistry(registry.Registry):
 
1833
    """Registry for log formatters"""
 
1834
 
 
1835
    def make_formatter(self, name, *args, **kwargs):
 
1836
        """Construct a formatter from arguments.
 
1837
 
 
1838
        :param name: Name of the formatter to construct.  'short', 'long' and
 
1839
            'line' are built-in.
 
1840
        """
 
1841
        return self.get(name)(*args, **kwargs)
 
1842
 
 
1843
    def get_default(self, branch):
 
1844
        c = branch.get_config_stack()
 
1845
        return self.get(c.get('log_format'))
 
1846
 
 
1847
 
 
1848
log_formatter_registry = LogFormatterRegistry()
 
1849
 
 
1850
 
 
1851
log_formatter_registry.register('short', ShortLogFormatter,
 
1852
                                'Moderately short log format.')
 
1853
log_formatter_registry.register('long', LongLogFormatter,
 
1854
                                'Detailed log format.')
 
1855
log_formatter_registry.register('line', LineLogFormatter,
 
1856
                                'Log format with one line per revision.')
 
1857
log_formatter_registry.register('gnu-changelog', GnuChangelogLogFormatter,
 
1858
                                'Format used by GNU ChangeLog files.')
 
1859
 
 
1860
 
 
1861
def register_formatter(name, formatter):
 
1862
    log_formatter_registry.register(name, formatter)
 
1863
 
 
1864
 
 
1865
def log_formatter(name, *args, **kwargs):
 
1866
    """Construct a formatter from arguments.
 
1867
 
 
1868
    name -- Name of the formatter to construct; currently 'long', 'short' and
 
1869
        'line' are supported.
 
1870
    """
 
1871
    try:
 
1872
        return log_formatter_registry.make_formatter(name, *args, **kwargs)
 
1873
    except KeyError:
 
1874
        raise errors.BzrCommandError(
 
1875
            gettext("unknown log formatter: %r") % name)
 
1876
 
 
1877
 
 
1878
def author_list_all(rev):
 
1879
    return rev.get_apparent_authors()[:]
 
1880
 
 
1881
 
 
1882
def author_list_first(rev):
 
1883
    lst = rev.get_apparent_authors()
 
1884
    try:
 
1885
        return [lst[0]]
 
1886
    except IndexError:
 
1887
        return []
 
1888
 
 
1889
 
 
1890
def author_list_committer(rev):
 
1891
    return [rev.committer]
 
1892
 
 
1893
 
 
1894
author_list_registry = registry.Registry()
 
1895
 
 
1896
author_list_registry.register('all', author_list_all,
 
1897
                              'All authors')
 
1898
 
 
1899
author_list_registry.register('first', author_list_first,
 
1900
                              'The first author')
 
1901
 
 
1902
author_list_registry.register('committer', author_list_committer,
 
1903
                              'The committer')
 
1904
 
 
1905
 
 
1906
def show_changed_revisions(branch, old_rh, new_rh, to_file=None,
 
1907
                           log_format='long'):
 
1908
    """Show the change in revision history comparing the old revision history to the new one.
 
1909
 
 
1910
    :param branch: The branch where the revisions exist
 
1911
    :param old_rh: The old revision history
 
1912
    :param new_rh: The new revision history
 
1913
    :param to_file: A file to write the results to. If None, stdout will be used
 
1914
    """
 
1915
    if to_file is None:
 
1916
        to_file = codecs.getwriter(get_terminal_encoding())(sys.stdout,
 
1917
                                                            errors='replace')
 
1918
    lf = log_formatter(log_format,
 
1919
                       show_ids=False,
 
1920
                       to_file=to_file,
 
1921
                       show_timezone='original')
 
1922
 
 
1923
    # This is the first index which is different between
 
1924
    # old and new
 
1925
    base_idx = None
 
1926
    for i in range(max(len(new_rh), len(old_rh))):
 
1927
        if (len(new_rh) <= i
 
1928
            or len(old_rh) <= i
 
1929
                or new_rh[i] != old_rh[i]):
 
1930
            base_idx = i
 
1931
            break
 
1932
 
 
1933
    if base_idx is None:
 
1934
        to_file.write('Nothing seems to have changed\n')
 
1935
        return
 
1936
    # TODO: It might be nice to do something like show_log
 
1937
    # and show the merged entries. But since this is the
 
1938
    # removed revisions, it shouldn't be as important
 
1939
    if base_idx < len(old_rh):
 
1940
        to_file.write('*' * 60)
 
1941
        to_file.write('\nRemoved Revisions:\n')
 
1942
        for i in range(base_idx, len(old_rh)):
 
1943
            rev = branch.repository.get_revision(old_rh[i])
 
1944
            lr = LogRevision(rev, i + 1, 0, None)
 
1945
            lf.log_revision(lr)
 
1946
        to_file.write('*' * 60)
 
1947
        to_file.write('\n\n')
 
1948
    if base_idx < len(new_rh):
 
1949
        to_file.write('Added Revisions:\n')
 
1950
        show_log(branch,
 
1951
                 lf,
 
1952
                 None,
 
1953
                 verbose=False,
 
1954
                 direction='forward',
 
1955
                 start_revision=base_idx + 1,
 
1956
                 end_revision=len(new_rh),
 
1957
                 search=None)
 
1958
 
 
1959
 
 
1960
def get_history_change(old_revision_id, new_revision_id, repository):
 
1961
    """Calculate the uncommon lefthand history between two revisions.
 
1962
 
 
1963
    :param old_revision_id: The original revision id.
 
1964
    :param new_revision_id: The new revision id.
 
1965
    :param repository: The repository to use for the calculation.
 
1966
 
 
1967
    return old_history, new_history
 
1968
    """
 
1969
    old_history = []
 
1970
    old_revisions = set()
 
1971
    new_history = []
 
1972
    new_revisions = set()
 
1973
    graph = repository.get_graph()
 
1974
    new_iter = graph.iter_lefthand_ancestry(new_revision_id)
 
1975
    old_iter = graph.iter_lefthand_ancestry(old_revision_id)
 
1976
    stop_revision = None
 
1977
    do_old = True
 
1978
    do_new = True
 
1979
    while do_new or do_old:
 
1980
        if do_new:
 
1981
            try:
 
1982
                new_revision = next(new_iter)
 
1983
            except StopIteration:
 
1984
                do_new = False
 
1985
            else:
 
1986
                new_history.append(new_revision)
 
1987
                new_revisions.add(new_revision)
 
1988
                if new_revision in old_revisions:
 
1989
                    stop_revision = new_revision
 
1990
                    break
 
1991
        if do_old:
 
1992
            try:
 
1993
                old_revision = next(old_iter)
 
1994
            except StopIteration:
 
1995
                do_old = False
 
1996
            else:
 
1997
                old_history.append(old_revision)
 
1998
                old_revisions.add(old_revision)
 
1999
                if old_revision in new_revisions:
 
2000
                    stop_revision = old_revision
 
2001
                    break
 
2002
    new_history.reverse()
 
2003
    old_history.reverse()
 
2004
    if stop_revision is not None:
 
2005
        new_history = new_history[new_history.index(stop_revision) + 1:]
 
2006
        old_history = old_history[old_history.index(stop_revision) + 1:]
 
2007
    return old_history, new_history
 
2008
 
 
2009
 
 
2010
def show_branch_change(branch, output, old_revno, old_revision_id):
 
2011
    """Show the changes made to a branch.
 
2012
 
 
2013
    :param branch: The branch to show changes about.
 
2014
    :param output: A file-like object to write changes to.
 
2015
    :param old_revno: The revno of the old tip.
 
2016
    :param old_revision_id: The revision_id of the old tip.
 
2017
    """
 
2018
    new_revno, new_revision_id = branch.last_revision_info()
 
2019
    old_history, new_history = get_history_change(old_revision_id,
 
2020
                                                  new_revision_id,
 
2021
                                                  branch.repository)
 
2022
    if old_history == [] and new_history == []:
 
2023
        output.write('Nothing seems to have changed\n')
 
2024
        return
 
2025
 
 
2026
    log_format = log_formatter_registry.get_default(branch)
 
2027
    lf = log_format(show_ids=False, to_file=output, show_timezone='original')
 
2028
    if old_history != []:
 
2029
        output.write('*' * 60)
 
2030
        output.write('\nRemoved Revisions:\n')
 
2031
        show_flat_log(branch.repository, old_history, old_revno, lf)
 
2032
        output.write('*' * 60)
 
2033
        output.write('\n\n')
 
2034
    if new_history != []:
 
2035
        output.write('Added Revisions:\n')
 
2036
        start_revno = new_revno - len(new_history) + 1
 
2037
        show_log(branch, lf, None, verbose=False, direction='forward',
 
2038
                 start_revision=start_revno)
 
2039
 
 
2040
 
 
2041
def show_flat_log(repository, history, last_revno, lf):
 
2042
    """Show a simple log of the specified history.
 
2043
 
 
2044
    :param repository: The repository to retrieve revisions from.
 
2045
    :param history: A list of revision_ids indicating the lefthand history.
 
2046
    :param last_revno: The revno of the last revision_id in the history.
 
2047
    :param lf: The log formatter to use.
 
2048
    """
 
2049
    revisions = repository.get_revisions(history)
 
2050
    for i, rev in enumerate(revisions):
 
2051
        lr = LogRevision(rev, i + last_revno, 0, None)
 
2052
        lf.log_revision(lr)
 
2053
 
 
2054
 
 
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.
 
2057
 
 
2058
    We search for files at the end of the range. If not found there,
 
2059
    we try the start of the range.
 
2060
 
 
2061
    :param revisionspec_list: revision range as parsed on the command line
 
2062
    :param file_list: the list of paths given on the command line;
 
2063
      the first of these can be a branch location or a file path,
 
2064
      the remainder must be file paths
 
2065
    :param add_cleanup: When the branch returned is read locked,
 
2066
      an unlock call will be queued to the cleanup.
 
2067
    :return: (branch, info_list, start_rev_info, end_rev_info) where
 
2068
      info_list is a list of (relative_path, file_id, kind) tuples where
 
2069
      kind is one of values 'directory', 'file', 'symlink', 'tree-reference'.
 
2070
      branch will be read-locked.
 
2071
    """
 
2072
    from breezy.builtins import _get_revision_range
 
2073
    tree, b, path = controldir.ControlDir.open_containing_tree_or_branch(
 
2074
        file_list[0])
 
2075
    add_cleanup(b.lock_read().unlock)
 
2076
    # XXX: It's damn messy converting a list of paths to relative paths when
 
2077
    # those paths might be deleted ones, they might be on a case-insensitive
 
2078
    # filesystem and/or they might be in silly locations (like another branch).
 
2079
    # For example, what should "log bzr://branch/dir/file1 file2" do? (Is
 
2080
    # file2 implicitly in the same dir as file1 or should its directory be
 
2081
    # taken from the current tree somehow?) For now, this solves the common
 
2082
    # case of running log in a nested directory, assuming paths beyond the
 
2083
    # first one haven't been deleted ...
 
2084
    if tree:
 
2085
        relpaths = [path] + tree.safe_relpath_files(file_list[1:])
 
2086
    else:
 
2087
        relpaths = [path] + file_list[1:]
 
2088
    info_list = []
 
2089
    start_rev_info, end_rev_info = _get_revision_range(revisionspec_list, b,
 
2090
                                                       "log")
 
2091
    if relpaths in ([], [u'']):
 
2092
        return b, [], start_rev_info, end_rev_info
 
2093
    if start_rev_info is None and end_rev_info is None:
 
2094
        if tree is None:
 
2095
            tree = b.basis_tree()
 
2096
        tree1 = None
 
2097
        for fp in relpaths:
 
2098
            file_id = tree.path2id(fp)
 
2099
            kind = _get_kind_for_file_id(tree, fp, file_id)
 
2100
            if file_id is None:
 
2101
                # go back to when time began
 
2102
                if tree1 is None:
 
2103
                    try:
 
2104
                        rev1 = b.get_rev_id(1)
 
2105
                    except errors.NoSuchRevision:
 
2106
                        # No history at all
 
2107
                        file_id = None
 
2108
                        kind = None
 
2109
                    else:
 
2110
                        tree1 = b.repository.revision_tree(rev1)
 
2111
                if tree1:
 
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))
 
2115
 
 
2116
    elif start_rev_info == end_rev_info:
 
2117
        # One revision given - file must exist in it
 
2118
        tree = b.repository.revision_tree(end_rev_info.rev_id)
 
2119
        for fp in relpaths:
 
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))
 
2123
 
 
2124
    else:
 
2125
        # Revision range given. Get the file-id from the end tree.
 
2126
        # If that fails, try the start tree.
 
2127
        rev_id = end_rev_info.rev_id
 
2128
        if rev_id is None:
 
2129
            tree = b.basis_tree()
 
2130
        else:
 
2131
            tree = b.repository.revision_tree(rev_id)
 
2132
        tree1 = None
 
2133
        for fp in relpaths:
 
2134
            file_id = tree.path2id(fp)
 
2135
            kind = _get_kind_for_file_id(tree, fp, file_id)
 
2136
            if file_id is None:
 
2137
                if tree1 is None:
 
2138
                    rev_id = start_rev_info.rev_id
 
2139
                    if rev_id is None:
 
2140
                        rev1 = b.get_rev_id(1)
 
2141
                        tree1 = b.repository.revision_tree(rev1)
 
2142
                    else:
 
2143
                        tree1 = b.repository.revision_tree(rev_id)
 
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))
 
2147
    return b, info_list, start_rev_info, end_rev_info
 
2148
 
 
2149
 
 
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
 
2156
 
 
2157
 
 
2158
properties_handler_registry = registry.Registry()
 
2159
 
 
2160
# Use the properties handlers to print out bug information if available
 
2161
 
 
2162
 
 
2163
def _bugs_properties_handler(revision):
 
2164
    fixed_bug_urls = []
 
2165
    related_bug_urls = []
 
2166
    for bug_url, status in revision.iter_bugs():
 
2167
        if status == 'fixed':
 
2168
            fixed_bug_urls.append(bug_url)
 
2169
        elif status == 'related':
 
2170
            related_bug_urls.append(bug_url)
 
2171
    ret = {}
 
2172
    if fixed_bug_urls:
 
2173
        text = ngettext('fixes bug', 'fixes bugs', len(fixed_bug_urls))
 
2174
        ret[text] = ' '.join(fixed_bug_urls)
 
2175
    if related_bug_urls:
 
2176
        text = ngettext('related bug', 'related bugs',
 
2177
                        len(related_bug_urls))
 
2178
        ret[text] = ' '.join(related_bug_urls)
 
2179
    return ret
 
2180
 
 
2181
 
 
2182
properties_handler_registry.register('bugs_properties_handler',
 
2183
                                     _bugs_properties_handler)
 
2184
 
 
2185
 
 
2186
# adapters which revision ids to log are filtered. When log is called, the
 
2187
# log_rev_iterator is adapted through each of these factory methods.
 
2188
# Plugins are welcome to mutate this list in any way they like - as long
 
2189
# as the overall behaviour is preserved. At this point there is no extensible
 
2190
# mechanism for getting parameters to each factory method, and until there is
 
2191
# this won't be considered a stable api.
 
2192
log_adapters = [
 
2193
    # core log logic
 
2194
    _make_batch_filter,
 
2195
    # read revision objects
 
2196
    _make_revision_objects,
 
2197
    # filter on log messages
 
2198
    _make_search_filter,
 
2199
    # generate deltas for things we will show
 
2200
    _make_delta_filter
 
2201
    ]