~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/log.py

  • Committer: Patch Queue Manager
  • Date: 2011-09-22 14:12:18 UTC
  • mfrom: (6155.3.1 jam)
  • Revision ID: pqm@pqm.ubuntu.com-20110922141218-86s4uu6nqvourw4f
(jameinel) Cleanup comments bzrlib/smart/__init__.py (John A Meinel)

Show diffs side-by-side

added added

removed removed

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