/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

  • Committer: Colin D Bennett
  • Date: 2009-01-30 08:24:38 UTC
  • mto: (3996.1.1 ianc-integration)
  • mto: This revision was merged to the branch mainline in revision 3997.
  • Revision ID: colin@gibibit.com-20090130082438-hl7t8y6z6feejnnc
Show all pending merge revisions in the commit message template.
Instead of showing only the merge tip revisions, show all pending merge
revisions like 'status -v' does, since it will not cause important
information to scroll off the screen as might happen in the status command.

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
    ]