/brz/remove-bazaar

To get this branch, use:
bzr branch http://gegoxaren.bato24.eu/bzr/brz/remove-bazaar

« back to all changes in this revision

Viewing changes to breezy/log.py

  • Committer: Jelmer Vernooij
  • Date: 2020-06-23 01:02:30 UTC
  • mfrom: (7490.40.27 work)
  • mto: This revision was merged to the branch mainline in revision 7517.
  • Revision ID: jelmer@jelmer.uk-20200623010230-62nnywznmb76h6ut
Merge lp:brz/3.1.

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