/brz/remove-bazaar

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

« back to all changes in this revision

Viewing changes to breezy/log.py

  • Committer: Jelmer Vernooij
  • Date: 2017-08-07 11:49:46 UTC
  • mto: (6747.3.4 avoid-set-revid-3)
  • mto: This revision was merged to the branch mainline in revision 6750.
  • Revision ID: jelmer@jelmer.uk-20170807114946-luclmxuawyzhpiot
Avoid setting revision_ids.

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