/brz/remove-bazaar

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

« back to all changes in this revision

Viewing changes to bzrlib/log.py

Handle submodules explicitly.

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2005, 2006, 2007 Canonical Ltd
2
 
#
3
 
# This program is free software; you can redistribute it and/or modify
4
 
# it under the terms of the GNU General Public License as published by
5
 
# the Free Software Foundation; either version 2 of the License, or
6
 
# (at your option) any later version.
7
 
#
8
 
# This program is distributed in the hope that it will be useful,
9
 
# but WITHOUT ANY WARRANTY; without even the implied warranty of
10
 
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11
 
# GNU General Public License for more details.
12
 
#
13
 
# You should have received a copy of the GNU General Public License
14
 
# along with this program; if not, write to the Free Software
15
 
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
16
 
 
17
 
 
18
 
 
19
 
"""Code to show logs of changes.
20
 
 
21
 
Various flavors of log can be produced:
22
 
 
23
 
* for one file, or the whole tree, and (not done yet) for
24
 
  files in a given directory
25
 
 
26
 
* in "verbose" mode with a description of what changed from one
27
 
  version to the next
28
 
 
29
 
* with file-ids and revision-ids shown
30
 
 
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.
50
 
"""
51
 
 
52
 
import codecs
53
 
from cStringIO import StringIO
54
 
from itertools import (
55
 
    izip,
56
 
    )
57
 
import re
58
 
import sys
59
 
from warnings import (
60
 
    warn,
61
 
    )
62
 
 
63
 
from bzrlib.lazy_import import lazy_import
64
 
lazy_import(globals(), """
65
 
 
66
 
from bzrlib import (
67
 
    config,
68
 
    diff,
69
 
    errors,
70
 
    repository as _mod_repository,
71
 
    revision as _mod_revision,
72
 
    revisionspec,
73
 
    trace,
74
 
    tsort,
75
 
    )
76
 
""")
77
 
 
78
 
from bzrlib import (
79
 
    registry,
80
 
    )
81
 
from bzrlib.osutils import (
82
 
    format_date,
83
 
    get_terminal_encoding,
84
 
    terminal_width,
85
 
    )
86
 
 
87
 
 
88
 
def find_touching_revisions(branch, file_id):
89
 
    """Yield a description of revisions which affect the file_id.
90
 
 
91
 
    Each returned element is (revno, revision_id, description)
92
 
 
93
 
    This is the list of revisions where the file is either added,
94
 
    modified, renamed or deleted.
95
 
 
96
 
    TODO: Perhaps some way to limit this to only particular revisions,
97
 
    or to traverse a non-mainline set of revisions?
98
 
    """
99
 
    last_ie = None
100
 
    last_path = None
101
 
    revno = 1
102
 
    for revision_id in branch.revision_history():
103
 
        this_inv = branch.repository.get_revision_inventory(revision_id)
104
 
        if file_id in this_inv:
105
 
            this_ie = this_inv[file_id]
106
 
            this_path = this_inv.id2path(file_id)
107
 
        else:
108
 
            this_ie = this_path = None
109
 
 
110
 
        # now we know how it was last time, and how it is in this revision.
111
 
        # are those two states effectively the same or not?
112
 
 
113
 
        if not this_ie and not last_ie:
114
 
            # not present in either
115
 
            pass
116
 
        elif this_ie and not last_ie:
117
 
            yield revno, revision_id, "added " + this_path
118
 
        elif not this_ie and last_ie:
119
 
            # deleted here
120
 
            yield revno, revision_id, "deleted " + last_path
121
 
        elif this_path != last_path:
122
 
            yield revno, revision_id, ("renamed %s => %s" % (last_path, this_path))
123
 
        elif (this_ie.text_size != last_ie.text_size
124
 
              or this_ie.text_sha1 != last_ie.text_sha1):
125
 
            yield revno, revision_id, "modified " + this_path
126
 
 
127
 
        last_ie = this_ie
128
 
        last_path = this_path
129
 
        revno += 1
130
 
 
131
 
 
132
 
def _enumerate_history(branch):
133
 
    rh = []
134
 
    revno = 1
135
 
    for rev_id in branch.revision_history():
136
 
        rh.append((revno, rev_id))
137
 
        revno += 1
138
 
    return rh
139
 
 
140
 
 
141
 
def show_log(branch,
142
 
             lf,
143
 
             specific_fileid=None,
144
 
             verbose=False,
145
 
             direction='reverse',
146
 
             start_revision=None,
147
 
             end_revision=None,
148
 
             search=None,
149
 
             limit=None,
150
 
             show_diff=False):
151
 
    """Write out human-readable log of commits to this branch.
152
 
 
153
 
    :param lf: The LogFormatter object showing the output.
154
 
 
155
 
    :param specific_fileid: If not None, list only the commits affecting the
156
 
        specified file, rather than all commits.
157
 
 
158
 
    :param verbose: If True show added/changed/deleted/renamed files.
159
 
 
160
 
    :param direction: 'reverse' (default) is latest to earliest; 'forward' is
161
 
        earliest to latest.
162
 
 
163
 
    :param start_revision: If not None, only show revisions >= start_revision
164
 
 
165
 
    :param end_revision: If not None, only show revisions <= end_revision
166
 
 
167
 
    :param search: If not None, only show revisions with matching commit
168
 
        messages
169
 
 
170
 
    :param limit: If set, shows only 'limit' revisions, all revisions are shown
171
 
        if None or 0.
172
 
 
173
 
    :param show_diff: If True, output a diff after each revision.
174
 
    """
175
 
    branch.lock_read()
176
 
    try:
177
 
        if getattr(lf, 'begin_log', None):
178
 
            lf.begin_log()
179
 
 
180
 
        _show_log(branch, lf, specific_fileid, verbose, direction,
181
 
                  start_revision, end_revision, search, limit, show_diff)
182
 
 
183
 
        if getattr(lf, 'end_log', None):
184
 
            lf.end_log()
185
 
    finally:
186
 
        branch.unlock()
187
 
 
188
 
 
189
 
def _show_log(branch,
190
 
             lf,
191
 
             specific_fileid=None,
192
 
             verbose=False,
193
 
             direction='reverse',
194
 
             start_revision=None,
195
 
             end_revision=None,
196
 
             search=None,
197
 
             limit=None,
198
 
             show_diff=False):
199
 
    """Worker function for show_log - see show_log."""
200
 
    if not isinstance(lf, LogFormatter):
201
 
        warn("not a LogFormatter instance: %r" % lf)
202
 
 
203
 
    if specific_fileid:
204
 
        trace.mutter('get log for file_id %r', specific_fileid)
205
 
    generate_merge_revisions = getattr(lf, 'supports_merge_revisions', False)
206
 
    allow_single_merge_revision = getattr(lf,
207
 
        'supports_single_merge_revision', False)
208
 
    view_revisions = calculate_view_revisions(branch, start_revision,
209
 
                                              end_revision, direction,
210
 
                                              specific_fileid,
211
 
                                              generate_merge_revisions,
212
 
                                              allow_single_merge_revision)
213
 
    rev_tag_dict = {}
214
 
    generate_tags = getattr(lf, 'supports_tags', False)
215
 
    if generate_tags:
216
 
        if branch.supports_tags():
217
 
            rev_tag_dict = branch.tags.get_reverse_tag_dict()
218
 
 
219
 
    generate_delta = verbose and getattr(lf, 'supports_delta', False)
220
 
    generate_diff = show_diff and getattr(lf, 'supports_diff', False)
221
 
 
222
 
    # now we just print all the revisions
223
 
    repo = branch.repository
224
 
    log_count = 0
225
 
    revision_iterator = make_log_rev_iterator(branch, view_revisions,
226
 
        generate_delta, search)
227
 
    for revs in revision_iterator:
228
 
        for (rev_id, revno, merge_depth), rev, delta in revs:
229
 
            if generate_diff:
230
 
                diff = _format_diff(repo, rev, rev_id, specific_fileid)
231
 
            else:
232
 
                diff = None
233
 
            lr = LogRevision(rev, revno, merge_depth, delta,
234
 
                             rev_tag_dict.get(rev_id), diff)
235
 
            lf.log_revision(lr)
236
 
            if limit:
237
 
                log_count += 1
238
 
                if log_count >= limit:
239
 
                    return
240
 
 
241
 
 
242
 
def _format_diff(repo, rev, rev_id, specific_fileid):
243
 
    if len(rev.parent_ids) == 0:
244
 
        ancestor_id = _mod_revision.NULL_REVISION
245
 
    else:
246
 
        ancestor_id = rev.parent_ids[0]
247
 
    tree_1 = repo.revision_tree(ancestor_id)
248
 
    tree_2 = repo.revision_tree(rev_id)
249
 
    if specific_fileid:
250
 
        specific_files = [tree_2.id2path(specific_fileid)]
251
 
    else:
252
 
        specific_files = None
253
 
    s = StringIO()
254
 
    diff.show_diff_trees(tree_1, tree_2, s, specific_files, old_label='',
255
 
        new_label='')
256
 
    return s.getvalue()
257
 
 
258
 
 
259
 
def calculate_view_revisions(branch, start_revision, end_revision, direction,
260
 
                             specific_fileid, generate_merge_revisions,
261
 
                             allow_single_merge_revision):
262
 
    if (    not generate_merge_revisions
263
 
        and start_revision is end_revision is None
264
 
        and direction == 'reverse'
265
 
        and specific_fileid is None):
266
 
        return _linear_view_revisions(branch)
267
 
 
268
 
    mainline_revs, rev_nos, start_rev_id, end_rev_id = _get_mainline_revs(
269
 
        branch, start_revision, end_revision)
270
 
    if not mainline_revs:
271
 
        return []
272
 
 
273
 
    generate_single_revision = False
274
 
    if ((not generate_merge_revisions)
275
 
        and ((start_rev_id and (start_rev_id not in rev_nos))
276
 
            or (end_rev_id and (end_rev_id not in rev_nos)))):
277
 
        generate_single_revision = ((start_rev_id == end_rev_id)
278
 
            and allow_single_merge_revision)
279
 
        if not generate_single_revision:
280
 
            raise errors.BzrCommandError('Selected log formatter only supports'
281
 
                ' mainline revisions.')
282
 
        generate_merge_revisions = generate_single_revision
283
 
    include_merges = generate_merge_revisions or specific_fileid
284
 
    view_revs_iter = get_view_revisions(mainline_revs, rev_nos, branch,
285
 
                          direction, include_merges=include_merges)
286
 
 
287
 
    if direction == 'reverse':
288
 
        start_rev_id, end_rev_id = end_rev_id, start_rev_id
289
 
    view_revisions = _filter_revision_range(list(view_revs_iter),
290
 
                                            start_rev_id,
291
 
                                            end_rev_id)
292
 
    if view_revisions and generate_single_revision:
293
 
        view_revisions = view_revisions[0:1]
294
 
    if specific_fileid:
295
 
        view_revisions = _filter_revisions_touching_file_id(branch,
296
 
            specific_fileid, view_revisions,
297
 
            include_merges=generate_merge_revisions)
298
 
 
299
 
    # rebase merge_depth - unless there are no revisions or 
300
 
    # either the first or last revision have merge_depth = 0.
301
 
    if view_revisions and view_revisions[0][2] and view_revisions[-1][2]:
302
 
        min_depth = min([d for r,n,d in view_revisions])
303
 
        if min_depth != 0:
304
 
            view_revisions = [(r,n,d-min_depth) for r,n,d in view_revisions]
305
 
    return view_revisions
306
 
 
307
 
 
308
 
def _linear_view_revisions(branch):
309
 
    start_revno, start_revision_id = branch.last_revision_info()
310
 
    repo = branch.repository
311
 
    revision_ids = repo.iter_reverse_revision_history(start_revision_id)
312
 
    for num, revision_id in enumerate(revision_ids):
313
 
        yield revision_id, str(start_revno - num), 0
314
 
 
315
 
 
316
 
def make_log_rev_iterator(branch, view_revisions, generate_delta, search):
317
 
    """Create a revision iterator for log.
318
 
 
319
 
    :param branch: The branch being logged.
320
 
    :param view_revisions: The revisions being viewed.
321
 
    :param generate_delta: Whether to generate a delta for each revision.
322
 
    :param search: A user text search string.
323
 
    :return: An iterator over lists of ((rev_id, revno, merge_depth), rev,
324
 
        delta).
325
 
    """
326
 
    # Convert view_revisions into (view, None, None) groups to fit with
327
 
    # the standard interface here.
328
 
    if type(view_revisions) == list:
329
 
        # A single batch conversion is faster than many incremental ones.
330
 
        # As we have all the data, do a batch conversion.
331
 
        nones = [None] * len(view_revisions)
332
 
        log_rev_iterator = iter([zip(view_revisions, nones, nones)])
333
 
    else:
334
 
        def _convert():
335
 
            for view in view_revisions:
336
 
                yield (view, None, None)
337
 
        log_rev_iterator = iter([_convert()])
338
 
    for adapter in log_adapters:
339
 
        log_rev_iterator = adapter(branch, generate_delta, search,
340
 
            log_rev_iterator)
341
 
    return log_rev_iterator
342
 
 
343
 
 
344
 
def _make_search_filter(branch, generate_delta, search, log_rev_iterator):
345
 
    """Create a filtered iterator of log_rev_iterator matching on a regex.
346
 
 
347
 
    :param branch: The branch being logged.
348
 
    :param generate_delta: Whether to generate a delta for each revision.
349
 
    :param search: A user text search string.
350
 
    :param log_rev_iterator: An input iterator containing all revisions that
351
 
        could be displayed, in lists.
352
 
    :return: An iterator over lists of ((rev_id, revno, merge_depth), rev,
353
 
        delta).
354
 
    """
355
 
    if search is None:
356
 
        return log_rev_iterator
357
 
    # Compile the search now to get early errors.
358
 
    searchRE = re.compile(search, re.IGNORECASE)
359
 
    return _filter_message_re(searchRE, log_rev_iterator)
360
 
 
361
 
 
362
 
def _filter_message_re(searchRE, log_rev_iterator):
363
 
    for revs in log_rev_iterator:
364
 
        new_revs = []
365
 
        for (rev_id, revno, merge_depth), rev, delta in revs:
366
 
            if searchRE.search(rev.message):
367
 
                new_revs.append(((rev_id, revno, merge_depth), rev, delta))
368
 
        yield new_revs
369
 
 
370
 
 
371
 
def _make_delta_filter(branch, generate_delta, search, log_rev_iterator):
372
 
    """Add revision deltas to a log iterator if needed.
373
 
 
374
 
    :param branch: The branch being logged.
375
 
    :param generate_delta: Whether to generate a delta for each revision.
376
 
    :param search: A user text search string.
377
 
    :param log_rev_iterator: An input iterator containing all revisions that
378
 
        could be displayed, in lists.
379
 
    :return: An iterator over lists of ((rev_id, revno, merge_depth), rev,
380
 
        delta).
381
 
    """
382
 
    if not generate_delta:
383
 
        return log_rev_iterator
384
 
    return _generate_deltas(branch.repository, log_rev_iterator)
385
 
 
386
 
 
387
 
def _generate_deltas(repository, log_rev_iterator):
388
 
    """Create deltas for each batch of revisions in log_rev_iterator."""
389
 
    for revs in log_rev_iterator:
390
 
        revisions = [rev[1] for rev in revs]
391
 
        deltas = repository.get_deltas_for_revisions(revisions)
392
 
        revs = [(rev[0], rev[1], delta) for rev, delta in izip(revs, deltas)]
393
 
        yield revs
394
 
 
395
 
 
396
 
def _make_revision_objects(branch, generate_delta, search, log_rev_iterator):
397
 
    """Extract revision objects from the repository
398
 
 
399
 
    :param branch: The branch being logged.
400
 
    :param generate_delta: Whether to generate a delta for each revision.
401
 
    :param search: A user text search string.
402
 
    :param log_rev_iterator: An input iterator containing all revisions that
403
 
        could be displayed, in lists.
404
 
    :return: An iterator over lists of ((rev_id, revno, merge_depth), rev,
405
 
        delta).
406
 
    """
407
 
    repository = branch.repository
408
 
    for revs in log_rev_iterator:
409
 
        # r = revision_id, n = revno, d = merge depth
410
 
        revision_ids = [view[0] for view, _, _ in revs]
411
 
        revisions = repository.get_revisions(revision_ids)
412
 
        revs = [(rev[0], revision, rev[2]) for rev, revision in
413
 
            izip(revs, revisions)]
414
 
        yield revs
415
 
 
416
 
 
417
 
def _make_batch_filter(branch, generate_delta, search, log_rev_iterator):
418
 
    """Group up a single large batch into smaller ones.
419
 
 
420
 
    :param branch: The branch being logged.
421
 
    :param generate_delta: Whether to generate a delta for each revision.
422
 
    :param search: A user text search string.
423
 
    :param log_rev_iterator: An input iterator containing all revisions that
424
 
        could be displayed, in lists.
425
 
    :return: An iterator over lists of ((rev_id, revno, merge_depth), rev,
426
 
        delta).
427
 
    """
428
 
    repository = branch.repository
429
 
    num = 9
430
 
    for batch in log_rev_iterator:
431
 
        batch = iter(batch)
432
 
        while True:
433
 
            step = [detail for _, detail in zip(range(num), batch)]
434
 
            if len(step) == 0:
435
 
                break
436
 
            yield step
437
 
            num = min(int(num * 1.5), 200)
438
 
 
439
 
 
440
 
def _get_mainline_revs(branch, start_revision, end_revision):
441
 
    """Get the mainline revisions from the branch.
442
 
    
443
 
    Generates the list of mainline revisions for the branch.
444
 
    
445
 
    :param  branch: The branch containing the revisions. 
446
 
 
447
 
    :param  start_revision: The first revision to be logged.
448
 
            For backwards compatibility this may be a mainline integer revno,
449
 
            but for merge revision support a RevisionInfo is expected.
450
 
 
451
 
    :param  end_revision: The last revision to be logged.
452
 
            For backwards compatibility this may be a mainline integer revno,
453
 
            but for merge revision support a RevisionInfo is expected.
454
 
 
455
 
    :return: A (mainline_revs, rev_nos, start_rev_id, end_rev_id) tuple.
456
 
    """
457
 
    branch_revno, branch_last_revision = branch.last_revision_info()
458
 
    if branch_revno == 0:
459
 
        return None, None, None, None
460
 
 
461
 
    # For mainline generation, map start_revision and end_revision to 
462
 
    # mainline revnos. If the revision is not on the mainline choose the 
463
 
    # appropriate extreme of the mainline instead - the extra will be 
464
 
    # filtered later.
465
 
    # Also map the revisions to rev_ids, to be used in the later filtering
466
 
    # stage.
467
 
    start_rev_id = None
468
 
    if start_revision is None:
469
 
        start_revno = 1
470
 
    else:
471
 
        if isinstance(start_revision, revisionspec.RevisionInfo):
472
 
            start_rev_id = start_revision.rev_id
473
 
            start_revno = start_revision.revno or 1
474
 
        else:
475
 
            branch.check_real_revno(start_revision)
476
 
            start_revno = start_revision
477
 
 
478
 
    end_rev_id = None
479
 
    if end_revision is None:
480
 
        end_revno = branch_revno
481
 
    else:
482
 
        if isinstance(end_revision, revisionspec.RevisionInfo):
483
 
            end_rev_id = end_revision.rev_id
484
 
            end_revno = end_revision.revno or branch_revno
485
 
        else:
486
 
            branch.check_real_revno(end_revision)
487
 
            end_revno = end_revision
488
 
 
489
 
    if ((start_rev_id == _mod_revision.NULL_REVISION)
490
 
        or (end_rev_id == _mod_revision.NULL_REVISION)):
491
 
        raise errors.BzrCommandError('Logging revision 0 is invalid.')
492
 
    if start_revno > end_revno:
493
 
        raise errors.BzrCommandError("Start revision must be older than "
494
 
                                     "the end revision.")
495
 
 
496
 
    if end_revno < start_revno:
497
 
        return None, None, None, None
498
 
    cur_revno = branch_revno
499
 
    rev_nos = {}
500
 
    mainline_revs = []
501
 
    for revision_id in branch.repository.iter_reverse_revision_history(
502
 
                        branch_last_revision):
503
 
        if cur_revno < start_revno:
504
 
            # We have gone far enough, but we always add 1 more revision
505
 
            rev_nos[revision_id] = cur_revno
506
 
            mainline_revs.append(revision_id)
507
 
            break
508
 
        if cur_revno <= end_revno:
509
 
            rev_nos[revision_id] = cur_revno
510
 
            mainline_revs.append(revision_id)
511
 
        cur_revno -= 1
512
 
    else:
513
 
        # We walked off the edge of all revisions, so we add a 'None' marker
514
 
        mainline_revs.append(None)
515
 
 
516
 
    mainline_revs.reverse()
517
 
 
518
 
    # override the mainline to look like the revision history.
519
 
    return mainline_revs, rev_nos, start_rev_id, end_rev_id
520
 
 
521
 
 
522
 
def _filter_revision_range(view_revisions, start_rev_id, end_rev_id):
523
 
    """Filter view_revisions based on revision ranges.
524
 
 
525
 
    :param view_revisions: A list of (revision_id, dotted_revno, merge_depth) 
526
 
            tuples to be filtered.
527
 
 
528
 
    :param start_rev_id: If not NONE specifies the first revision to be logged.
529
 
            If NONE then all revisions up to the end_rev_id are logged.
530
 
 
531
 
    :param end_rev_id: If not NONE specifies the last revision to be logged.
532
 
            If NONE then all revisions up to the end of the log are logged.
533
 
 
534
 
    :return: The filtered view_revisions.
535
 
    """
536
 
    if start_rev_id or end_rev_id:
537
 
        revision_ids = [r for r, n, d in view_revisions]
538
 
        if start_rev_id:
539
 
            start_index = revision_ids.index(start_rev_id)
540
 
        else:
541
 
            start_index = 0
542
 
        if start_rev_id == end_rev_id:
543
 
            end_index = start_index
544
 
        else:
545
 
            if end_rev_id:
546
 
                end_index = revision_ids.index(end_rev_id)
547
 
            else:
548
 
                end_index = len(view_revisions) - 1
549
 
        # To include the revisions merged into the last revision, 
550
 
        # extend end_rev_id down to, but not including, the next rev
551
 
        # with the same or lesser merge_depth
552
 
        end_merge_depth = view_revisions[end_index][2]
553
 
        try:
554
 
            for index in xrange(end_index+1, len(view_revisions)+1):
555
 
                if view_revisions[index][2] <= end_merge_depth:
556
 
                    end_index = index - 1
557
 
                    break
558
 
        except IndexError:
559
 
            # if the search falls off the end then log to the end as well
560
 
            end_index = len(view_revisions) - 1
561
 
        view_revisions = view_revisions[start_index:end_index+1]
562
 
    return view_revisions
563
 
 
564
 
 
565
 
def _filter_revisions_touching_file_id(branch, file_id, view_revisions,
566
 
    include_merges=True):
567
 
    r"""Return the list of revision ids which touch a given file id.
568
 
 
569
 
    The function filters view_revisions and returns a subset.
570
 
    This includes the revisions which directly change the file id,
571
 
    and the revisions which merge these changes. So if the
572
 
    revision graph is::
573
 
        A-.
574
 
        |\ \
575
 
        B C E
576
 
        |/ /
577
 
        D |
578
 
        |\|
579
 
        | F
580
 
        |/
581
 
        G
582
 
 
583
 
    And 'C' changes a file, then both C and D will be returned. F will not be
584
 
    returned even though it brings the changes to C into the branch starting
585
 
    with E. (Note that if we were using F as the tip instead of G, then we
586
 
    would see C, D, F.)
587
 
 
588
 
    This will also be restricted based on a subset of the mainline.
589
 
 
590
 
    :param branch: The branch where we can get text revision information.
591
 
 
592
 
    :param file_id: Filter out revisions that do not touch file_id.
593
 
 
594
 
    :param view_revisions: A list of (revision_id, dotted_revno, merge_depth)
595
 
        tuples. This is the list of revisions which will be filtered. It is
596
 
        assumed that view_revisions is in merge_sort order (i.e. newest
597
 
        revision first ).
598
 
 
599
 
    :param include_merges: include merge revisions in the result or not
600
 
 
601
 
    :return: A list of (revision_id, dotted_revno, merge_depth) tuples.
602
 
    """
603
 
    # Lookup all possible text keys to determine which ones actually modified
604
 
    # the file.
605
 
    text_keys = [(file_id, rev_id) for rev_id, revno, depth in view_revisions]
606
 
    # Looking up keys in batches of 1000 can cut the time in half, as well as
607
 
    # memory consumption. GraphIndex *does* like to look for a few keys in
608
 
    # parallel, it just doesn't like looking for *lots* of keys in parallel.
609
 
    # TODO: This code needs to be re-evaluated periodically as we tune the
610
 
    #       indexing layer. We might consider passing in hints as to the known
611
 
    #       access pattern (sparse/clustered, high success rate/low success
612
 
    #       rate). This particular access is clustered with a low success rate.
613
 
    get_parent_map = branch.repository.texts.get_parent_map
614
 
    modified_text_revisions = set()
615
 
    chunk_size = 1000
616
 
    for start in xrange(0, len(text_keys), chunk_size):
617
 
        next_keys = text_keys[start:start + chunk_size]
618
 
        # Only keep the revision_id portion of the key
619
 
        modified_text_revisions.update(
620
 
            [k[1] for k in get_parent_map(next_keys)])
621
 
    del text_keys, next_keys
622
 
 
623
 
    result = []
624
 
    # Track what revisions will merge the current revision, replace entries
625
 
    # with 'None' when they have been added to result
626
 
    current_merge_stack = [None]
627
 
    for info in view_revisions:
628
 
        rev_id, revno, depth = info
629
 
        if depth == len(current_merge_stack):
630
 
            current_merge_stack.append(info)
631
 
        else:
632
 
            del current_merge_stack[depth + 1:]
633
 
            current_merge_stack[-1] = info
634
 
 
635
 
        if rev_id in modified_text_revisions:
636
 
            # This needs to be logged, along with the extra revisions
637
 
            for idx in xrange(len(current_merge_stack)):
638
 
                node = current_merge_stack[idx]
639
 
                if node is not None:
640
 
                    if include_merges or node[2] == 0:
641
 
                        result.append(node)
642
 
                        current_merge_stack[idx] = None
643
 
    return result
644
 
 
645
 
 
646
 
def get_view_revisions(mainline_revs, rev_nos, branch, direction,
647
 
                       include_merges=True):
648
 
    """Produce an iterator of revisions to show
649
 
    :return: an iterator of (revision_id, revno, merge_depth)
650
 
    (if there is no revno for a revision, None is supplied)
651
 
    """
652
 
    if not include_merges:
653
 
        revision_ids = mainline_revs[1:]
654
 
        if direction == 'reverse':
655
 
            revision_ids.reverse()
656
 
        for revision_id in revision_ids:
657
 
            yield revision_id, str(rev_nos[revision_id]), 0
658
 
        return
659
 
    graph = branch.repository.get_graph()
660
 
    # This asks for all mainline revisions, which means we only have to spider
661
 
    # sideways, rather than depth history. That said, its still size-of-history
662
 
    # and should be addressed.
663
 
    # mainline_revisions always includes an extra revision at the beginning, so
664
 
    # don't request it.
665
 
    parent_map = dict(((key, value) for key, value in
666
 
        graph.iter_ancestry(mainline_revs[1:]) if value is not None))
667
 
    # filter out ghosts; merge_sort errors on ghosts.
668
 
    rev_graph = _mod_repository._strip_NULL_ghosts(parent_map)
669
 
    merge_sorted_revisions = tsort.merge_sort(
670
 
        rev_graph,
671
 
        mainline_revs[-1],
672
 
        mainline_revs,
673
 
        generate_revno=True)
674
 
 
675
 
    if direction == 'forward':
676
 
        # forward means oldest first.
677
 
        merge_sorted_revisions = reverse_by_depth(merge_sorted_revisions)
678
 
    elif direction != 'reverse':
679
 
        raise ValueError('invalid direction %r' % direction)
680
 
 
681
 
    for (sequence, rev_id, merge_depth, revno, end_of_merge
682
 
         ) in merge_sorted_revisions:
683
 
        yield rev_id, '.'.join(map(str, revno)), merge_depth
684
 
 
685
 
 
686
 
def reverse_by_depth(merge_sorted_revisions, _depth=0):
687
 
    """Reverse revisions by depth.
688
 
 
689
 
    Revisions with a different depth are sorted as a group with the previous
690
 
    revision of that depth.  There may be no topological justification for this,
691
 
    but it looks much nicer.
692
 
    """
693
 
    # Add a fake revision at start so that we can always attach sub revisions
694
 
    merge_sorted_revisions = [(None, None, _depth)] + merge_sorted_revisions
695
 
    zd_revisions = []
696
 
    for val in merge_sorted_revisions:
697
 
        if val[2] == _depth:
698
 
            # Each revision at the current depth becomes a chunk grouping all
699
 
            # higher depth revisions.
700
 
            zd_revisions.append([val])
701
 
        else:
702
 
            zd_revisions[-1].append(val)
703
 
    for revisions in zd_revisions:
704
 
        if len(revisions) > 1:
705
 
            # We have higher depth revisions, let reverse them locally
706
 
            revisions[1:] = reverse_by_depth(revisions[1:], _depth + 1)
707
 
    zd_revisions.reverse()
708
 
    result = []
709
 
    for chunk in zd_revisions:
710
 
        result.extend(chunk)
711
 
    if _depth == 0:
712
 
        # Top level call, get rid of the fake revisions that have been added
713
 
        result = [r for r in result if r[0] is not None and r[1] is not None]
714
 
    return result
715
 
 
716
 
 
717
 
class LogRevision(object):
718
 
    """A revision to be logged (by LogFormatter.log_revision).
719
 
 
720
 
    A simple wrapper for the attributes of a revision to be logged.
721
 
    The attributes may or may not be populated, as determined by the 
722
 
    logging options and the log formatter capabilities.
723
 
    """
724
 
 
725
 
    def __init__(self, rev=None, revno=None, merge_depth=0, delta=None,
726
 
                 tags=None, diff=None):
727
 
        self.rev = rev
728
 
        self.revno = revno
729
 
        self.merge_depth = merge_depth
730
 
        self.delta = delta
731
 
        self.tags = tags
732
 
        self.diff = diff
733
 
 
734
 
 
735
 
class LogFormatter(object):
736
 
    """Abstract class to display log messages.
737
 
 
738
 
    At a minimum, a derived class must implement the log_revision method.
739
 
 
740
 
    If the LogFormatter needs to be informed of the beginning or end of
741
 
    a log it should implement the begin_log and/or end_log hook methods.
742
 
 
743
 
    A LogFormatter should define the following supports_XXX flags 
744
 
    to indicate which LogRevision attributes it supports:
745
 
 
746
 
    - supports_delta must be True if this log formatter supports delta.
747
 
        Otherwise the delta attribute may not be populated.  The 'delta_format'
748
 
        attribute describes whether the 'short_status' format (1) or the long
749
 
        one (2) sould be used.
750
 
 
751
 
    - supports_merge_revisions must be True if this log formatter supports 
752
 
        merge revisions.  If not, and if supports_single_merge_revisions is
753
 
        also not True, then only mainline revisions will be passed to the 
754
 
        formatter.
755
 
 
756
 
    - supports_single_merge_revision must be True if this log formatter
757
 
        supports logging only a single merge revision.  This flag is
758
 
        only relevant if supports_merge_revisions is not True.
759
 
 
760
 
    - supports_tags must be True if this log formatter supports tags.
761
 
        Otherwise the tags attribute may not be populated.
762
 
 
763
 
    - supports_diff must be True if this log formatter supports diffs.
764
 
        Otherwise the diff attribute may not be populated.
765
 
 
766
 
    Plugins can register functions to show custom revision properties using
767
 
    the properties_handler_registry. The registered function
768
 
    must respect the following interface description:
769
 
        def my_show_properties(properties_dict):
770
 
            # code that returns a dict {'name':'value'} of the properties 
771
 
            # to be shown
772
 
    """
773
 
 
774
 
    def __init__(self, to_file, show_ids=False, show_timezone='original',
775
 
                 delta_format=None):
776
 
        self.to_file = to_file
777
 
        self.show_ids = show_ids
778
 
        self.show_timezone = show_timezone
779
 
        if delta_format is None:
780
 
            # Ensures backward compatibility
781
 
            delta_format = 2 # long format
782
 
        self.delta_format = delta_format
783
 
 
784
 
# TODO: uncomment this block after show() has been removed.
785
 
# Until then defining log_revision would prevent _show_log calling show() 
786
 
# in legacy formatters.
787
 
#    def log_revision(self, revision):
788
 
#        """Log a revision.
789
 
#
790
 
#        :param  revision:   The LogRevision to be logged.
791
 
#        """
792
 
#        raise NotImplementedError('not implemented in abstract base')
793
 
 
794
 
    def short_committer(self, rev):
795
 
        name, address = config.parse_username(rev.committer)
796
 
        if name:
797
 
            return name
798
 
        return address
799
 
 
800
 
    def short_author(self, rev):
801
 
        name, address = config.parse_username(rev.get_apparent_author())
802
 
        if name:
803
 
            return name
804
 
        return address
805
 
 
806
 
    def show_properties(self, revision, indent):
807
 
        """Displays the custom properties returned by each registered handler.
808
 
        
809
 
        If a registered handler raises an error it is propagated.
810
 
        """
811
 
        for key, handler in properties_handler_registry.iteritems():
812
 
            for key, value in handler(revision).items():
813
 
                self.to_file.write(indent + key + ': ' + value + '\n')
814
 
 
815
 
    def show_diff(self, to_file, diff, indent):
816
 
        for l in diff.rstrip().split('\n'):
817
 
            to_file.write(indent + '%s\n' % (l,))
818
 
 
819
 
 
820
 
class LongLogFormatter(LogFormatter):
821
 
 
822
 
    supports_merge_revisions = True
823
 
    supports_delta = True
824
 
    supports_tags = True
825
 
    supports_diff = True
826
 
 
827
 
    def log_revision(self, revision):
828
 
        """Log a revision, either merged or not."""
829
 
        indent = '    ' * revision.merge_depth
830
 
        to_file = self.to_file
831
 
        to_file.write(indent + '-' * 60 + '\n')
832
 
        if revision.revno is not None:
833
 
            to_file.write(indent + 'revno: %s\n' % (revision.revno,))
834
 
        if revision.tags:
835
 
            to_file.write(indent + 'tags: %s\n' % (', '.join(revision.tags)))
836
 
        if self.show_ids:
837
 
            to_file.write(indent + 'revision-id: ' + revision.rev.revision_id)
838
 
            to_file.write('\n')
839
 
            for parent_id in revision.rev.parent_ids:
840
 
                to_file.write(indent + 'parent: %s\n' % (parent_id,))
841
 
        self.show_properties(revision.rev, indent)
842
 
 
843
 
        author = revision.rev.properties.get('author', None)
844
 
        if author is not None:
845
 
            to_file.write(indent + 'author: %s\n' % (author,))
846
 
        to_file.write(indent + 'committer: %s\n' % (revision.rev.committer,))
847
 
 
848
 
        branch_nick = revision.rev.properties.get('branch-nick', None)
849
 
        if branch_nick is not None:
850
 
            to_file.write(indent + 'branch nick: %s\n' % (branch_nick,))
851
 
 
852
 
        date_str = format_date(revision.rev.timestamp,
853
 
                               revision.rev.timezone or 0,
854
 
                               self.show_timezone)
855
 
        to_file.write(indent + 'timestamp: %s\n' % (date_str,))
856
 
 
857
 
        to_file.write(indent + 'message:\n')
858
 
        if not revision.rev.message:
859
 
            to_file.write(indent + '  (no message)\n')
860
 
        else:
861
 
            message = revision.rev.message.rstrip('\r\n')
862
 
            for l in message.split('\n'):
863
 
                to_file.write(indent + '  %s\n' % (l,))
864
 
        if revision.delta is not None:
865
 
            # We don't respect delta_format for compatibility
866
 
            revision.delta.show(to_file, self.show_ids, indent=indent,
867
 
                                short_status=False)
868
 
        if revision.diff is not None:
869
 
            to_file.write(indent + 'diff:\n')
870
 
            # Note: we explicitly don't indent the diff (relative to the
871
 
            # revision information) so that the output can be fed to patch -p0
872
 
            self.show_diff(to_file, revision.diff, indent)
873
 
 
874
 
 
875
 
class ShortLogFormatter(LogFormatter):
876
 
 
877
 
    supports_delta = True
878
 
    supports_tags = True
879
 
    supports_single_merge_revision = True
880
 
    supports_diff = True
881
 
 
882
 
    def log_revision(self, revision):
883
 
        to_file = self.to_file
884
 
        is_merge = ''
885
 
        if len(revision.rev.parent_ids) > 1:
886
 
            is_merge = ' [merge]'
887
 
        tags = ''
888
 
        if revision.tags:
889
 
            tags = ' {%s}' % (', '.join(revision.tags))
890
 
 
891
 
        to_file.write("%5s %s\t%s%s%s\n" % (revision.revno,
892
 
                self.short_author(revision.rev),
893
 
                format_date(revision.rev.timestamp,
894
 
                            revision.rev.timezone or 0,
895
 
                            self.show_timezone, date_fmt="%Y-%m-%d",
896
 
                            show_offset=False),
897
 
                tags, is_merge))
898
 
        if self.show_ids:
899
 
            to_file.write('      revision-id:%s\n'
900
 
                          % (revision.rev.revision_id,))
901
 
        if not revision.rev.message:
902
 
            to_file.write('      (no message)\n')
903
 
        else:
904
 
            message = revision.rev.message.rstrip('\r\n')
905
 
            for l in message.split('\n'):
906
 
                to_file.write('      %s\n' % (l,))
907
 
 
908
 
        if revision.delta is not None:
909
 
            revision.delta.show(to_file, self.show_ids,
910
 
                                short_status=self.delta_format==1)
911
 
        if revision.diff is not None:
912
 
            self.show_diff(to_file, revision.diff, '      ')
913
 
        to_file.write('\n')
914
 
 
915
 
 
916
 
class LineLogFormatter(LogFormatter):
917
 
 
918
 
    supports_tags = True
919
 
    supports_single_merge_revision = True
920
 
 
921
 
    def __init__(self, *args, **kwargs):
922
 
        super(LineLogFormatter, self).__init__(*args, **kwargs)
923
 
        self._max_chars = terminal_width() - 1
924
 
 
925
 
    def truncate(self, str, max_len):
926
 
        if len(str) <= max_len:
927
 
            return str
928
 
        return str[:max_len-3]+'...'
929
 
 
930
 
    def date_string(self, rev):
931
 
        return format_date(rev.timestamp, rev.timezone or 0,
932
 
                           self.show_timezone, date_fmt="%Y-%m-%d",
933
 
                           show_offset=False)
934
 
 
935
 
    def message(self, rev):
936
 
        if not rev.message:
937
 
            return '(no message)'
938
 
        else:
939
 
            return rev.message
940
 
 
941
 
    def log_revision(self, revision):
942
 
        self.to_file.write(self.log_string(revision.revno, revision.rev,
943
 
            self._max_chars, revision.tags))
944
 
        self.to_file.write('\n')
945
 
 
946
 
    def log_string(self, revno, rev, max_chars, tags=None):
947
 
        """Format log info into one string. Truncate tail of string
948
 
        :param  revno:      revision number or None.
949
 
                            Revision numbers counts from 1.
950
 
        :param  rev:        revision object
951
 
        :param  max_chars:  maximum length of resulting string
952
 
        :param  tags:       list of tags or None
953
 
        :return:            formatted truncated string
954
 
        """
955
 
        out = []
956
 
        if revno:
957
 
            # show revno only when is not None
958
 
            out.append("%s:" % revno)
959
 
        out.append(self.truncate(self.short_author(rev), 20))
960
 
        out.append(self.date_string(rev))
961
 
        if tags:
962
 
            tag_str = '{%s}' % (', '.join(tags))
963
 
            out.append(tag_str)
964
 
        out.append(rev.get_summary())
965
 
        return self.truncate(" ".join(out).rstrip('\n'), max_chars)
966
 
 
967
 
 
968
 
def line_log(rev, max_chars):
969
 
    lf = LineLogFormatter(None)
970
 
    return lf.log_string(None, rev, max_chars)
971
 
 
972
 
 
973
 
class LogFormatterRegistry(registry.Registry):
974
 
    """Registry for log formatters"""
975
 
 
976
 
    def make_formatter(self, name, *args, **kwargs):
977
 
        """Construct a formatter from arguments.
978
 
 
979
 
        :param name: Name of the formatter to construct.  'short', 'long' and
980
 
            'line' are built-in.
981
 
        """
982
 
        return self.get(name)(*args, **kwargs)
983
 
 
984
 
    def get_default(self, branch):
985
 
        return self.get(branch.get_config().log_format())
986
 
 
987
 
 
988
 
log_formatter_registry = LogFormatterRegistry()
989
 
 
990
 
 
991
 
log_formatter_registry.register('short', ShortLogFormatter,
992
 
                                'Moderately short log format')
993
 
log_formatter_registry.register('long', LongLogFormatter,
994
 
                                'Detailed log format')
995
 
log_formatter_registry.register('line', LineLogFormatter,
996
 
                                'Log format with one line per revision')
997
 
 
998
 
 
999
 
def register_formatter(name, formatter):
1000
 
    log_formatter_registry.register(name, formatter)
1001
 
 
1002
 
 
1003
 
def log_formatter(name, *args, **kwargs):
1004
 
    """Construct a formatter from arguments.
1005
 
 
1006
 
    name -- Name of the formatter to construct; currently 'long', 'short' and
1007
 
        'line' are supported.
1008
 
    """
1009
 
    try:
1010
 
        return log_formatter_registry.make_formatter(name, *args, **kwargs)
1011
 
    except KeyError:
1012
 
        raise errors.BzrCommandError("unknown log formatter: %r" % name)
1013
 
 
1014
 
 
1015
 
def show_one_log(revno, rev, delta, verbose, to_file, show_timezone):
1016
 
    # deprecated; for compatibility
1017
 
    lf = LongLogFormatter(to_file=to_file, show_timezone=show_timezone)
1018
 
    lf.show(revno, rev, delta)
1019
 
 
1020
 
 
1021
 
def show_changed_revisions(branch, old_rh, new_rh, to_file=None,
1022
 
                           log_format='long'):
1023
 
    """Show the change in revision history comparing the old revision history to the new one.
1024
 
 
1025
 
    :param branch: The branch where the revisions exist
1026
 
    :param old_rh: The old revision history
1027
 
    :param new_rh: The new revision history
1028
 
    :param to_file: A file to write the results to. If None, stdout will be used
1029
 
    """
1030
 
    if to_file is None:
1031
 
        to_file = codecs.getwriter(get_terminal_encoding())(sys.stdout,
1032
 
            errors='replace')
1033
 
    lf = log_formatter(log_format,
1034
 
                       show_ids=False,
1035
 
                       to_file=to_file,
1036
 
                       show_timezone='original')
1037
 
 
1038
 
    # This is the first index which is different between
1039
 
    # old and new
1040
 
    base_idx = None
1041
 
    for i in xrange(max(len(new_rh),
1042
 
                        len(old_rh))):
1043
 
        if (len(new_rh) <= i
1044
 
            or len(old_rh) <= i
1045
 
            or new_rh[i] != old_rh[i]):
1046
 
            base_idx = i
1047
 
            break
1048
 
 
1049
 
    if base_idx is None:
1050
 
        to_file.write('Nothing seems to have changed\n')
1051
 
        return
1052
 
    ## TODO: It might be nice to do something like show_log
1053
 
    ##       and show the merged entries. But since this is the
1054
 
    ##       removed revisions, it shouldn't be as important
1055
 
    if base_idx < len(old_rh):
1056
 
        to_file.write('*'*60)
1057
 
        to_file.write('\nRemoved Revisions:\n')
1058
 
        for i in range(base_idx, len(old_rh)):
1059
 
            rev = branch.repository.get_revision(old_rh[i])
1060
 
            lr = LogRevision(rev, i+1, 0, None)
1061
 
            lf.log_revision(lr)
1062
 
        to_file.write('*'*60)
1063
 
        to_file.write('\n\n')
1064
 
    if base_idx < len(new_rh):
1065
 
        to_file.write('Added Revisions:\n')
1066
 
        show_log(branch,
1067
 
                 lf,
1068
 
                 None,
1069
 
                 verbose=False,
1070
 
                 direction='forward',
1071
 
                 start_revision=base_idx+1,
1072
 
                 end_revision=len(new_rh),
1073
 
                 search=None)
1074
 
 
1075
 
 
1076
 
def get_history_change(old_revision_id, new_revision_id, repository):
1077
 
    """Calculate the uncommon lefthand history between two revisions.
1078
 
 
1079
 
    :param old_revision_id: The original revision id.
1080
 
    :param new_revision_id: The new revision id.
1081
 
    :param repository: The repository to use for the calculation.
1082
 
 
1083
 
    return old_history, new_history
1084
 
    """
1085
 
    old_history = []
1086
 
    old_revisions = set()
1087
 
    new_history = []
1088
 
    new_revisions = set()
1089
 
    new_iter = repository.iter_reverse_revision_history(new_revision_id)
1090
 
    old_iter = repository.iter_reverse_revision_history(old_revision_id)
1091
 
    stop_revision = None
1092
 
    do_old = True
1093
 
    do_new = True
1094
 
    while do_new or do_old:
1095
 
        if do_new:
1096
 
            try:
1097
 
                new_revision = new_iter.next()
1098
 
            except StopIteration:
1099
 
                do_new = False
1100
 
            else:
1101
 
                new_history.append(new_revision)
1102
 
                new_revisions.add(new_revision)
1103
 
                if new_revision in old_revisions:
1104
 
                    stop_revision = new_revision
1105
 
                    break
1106
 
        if do_old:
1107
 
            try:
1108
 
                old_revision = old_iter.next()
1109
 
            except StopIteration:
1110
 
                do_old = False
1111
 
            else:
1112
 
                old_history.append(old_revision)
1113
 
                old_revisions.add(old_revision)
1114
 
                if old_revision in new_revisions:
1115
 
                    stop_revision = old_revision
1116
 
                    break
1117
 
    new_history.reverse()
1118
 
    old_history.reverse()
1119
 
    if stop_revision is not None:
1120
 
        new_history = new_history[new_history.index(stop_revision) + 1:]
1121
 
        old_history = old_history[old_history.index(stop_revision) + 1:]
1122
 
    return old_history, new_history
1123
 
 
1124
 
 
1125
 
def show_branch_change(branch, output, old_revno, old_revision_id):
1126
 
    """Show the changes made to a branch.
1127
 
 
1128
 
    :param branch: The branch to show changes about.
1129
 
    :param output: A file-like object to write changes to.
1130
 
    :param old_revno: The revno of the old tip.
1131
 
    :param old_revision_id: The revision_id of the old tip.
1132
 
    """
1133
 
    new_revno, new_revision_id = branch.last_revision_info()
1134
 
    old_history, new_history = get_history_change(old_revision_id,
1135
 
                                                  new_revision_id,
1136
 
                                                  branch.repository)
1137
 
    if old_history == [] and new_history == []:
1138
 
        output.write('Nothing seems to have changed\n')
1139
 
        return
1140
 
 
1141
 
    log_format = log_formatter_registry.get_default(branch)
1142
 
    lf = log_format(show_ids=False, to_file=output, show_timezone='original')
1143
 
    if old_history != []:
1144
 
        output.write('*'*60)
1145
 
        output.write('\nRemoved Revisions:\n')
1146
 
        show_flat_log(branch.repository, old_history, old_revno, lf)
1147
 
        output.write('*'*60)
1148
 
        output.write('\n\n')
1149
 
    if new_history != []:
1150
 
        output.write('Added Revisions:\n')
1151
 
        start_revno = new_revno - len(new_history) + 1
1152
 
        show_log(branch, lf, None, verbose=False, direction='forward',
1153
 
                 start_revision=start_revno,)
1154
 
 
1155
 
 
1156
 
def show_flat_log(repository, history, last_revno, lf):
1157
 
    """Show a simple log of the specified history.
1158
 
 
1159
 
    :param repository: The repository to retrieve revisions from.
1160
 
    :param history: A list of revision_ids indicating the lefthand history.
1161
 
    :param last_revno: The revno of the last revision_id in the history.
1162
 
    :param lf: The log formatter to use.
1163
 
    """
1164
 
    start_revno = last_revno - len(history) + 1
1165
 
    revisions = repository.get_revisions(history)
1166
 
    for i, rev in enumerate(revisions):
1167
 
        lr = LogRevision(rev, i + last_revno, 0, None)
1168
 
        lf.log_revision(lr)
1169
 
 
1170
 
 
1171
 
properties_handler_registry = registry.Registry()
1172
 
properties_handler_registry.register_lazy("foreign",
1173
 
                                          "bzrlib.foreign",
1174
 
                                          "show_foreign_properties")
1175
 
 
1176
 
 
1177
 
# adapters which revision ids to log are filtered. When log is called, the
1178
 
# log_rev_iterator is adapted through each of these factory methods.
1179
 
# Plugins are welcome to mutate this list in any way they like - as long
1180
 
# as the overall behaviour is preserved. At this point there is no extensible
1181
 
# mechanism for getting parameters to each factory method, and until there is
1182
 
# this won't be considered a stable api.
1183
 
log_adapters = [
1184
 
    # core log logic
1185
 
    _make_batch_filter,
1186
 
    # read revision objects
1187
 
    _make_revision_objects,
1188
 
    # filter on log messages
1189
 
    _make_search_filter,
1190
 
    # generate deltas for things we will show
1191
 
    _make_delta_filter
1192
 
    ]