/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: 2018-11-10 17:18:27 UTC
  • mto: (7143.11.2 unused-imports)
  • mto: This revision was merged to the branch mainline in revision 7144.
  • Revision ID: jelmer@jelmer.uk-20181110171827-46xer5sa9fzgab1q
Add flake8 configuration to monkey patch for lazy imports.

Show diffs side-by-side

added added

removed removed

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