~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/log.py

  • Committer: Aaron Bentley
  • Date: 2007-02-06 14:52:16 UTC
  • mfrom: (2266 +trunk)
  • mto: This revision was merged to the branch mainline in revision 2268.
  • Revision ID: abentley@panoramicfeedback.com-20070206145216-fcpi8o3ufvuzwbp9
Merge bzr.dev

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2005, 2006, 2007 Canonical Ltd
 
1
# Copyright (C) 2005 Canonical Ltd
2
2
#
3
3
# This program is free software; you can redistribute it and/or modify
4
4
# it under the terms of the GNU General Public License as published by
42
42
 
43
43
In verbose mode we show a summary of what changed in each particular
44
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
 
45
relative to its mainline parent, not the delta relative to the last
46
46
logged revision.  So for example if you ask for a verbose log of
47
47
changes touching hello.c you will get a list of those revisions also
48
48
listing other things that were changed in the same revision, but not
49
49
all the changes since the previous revision that touched hello.c.
50
50
"""
51
51
 
52
 
import codecs
53
 
from itertools import (
54
 
    izip,
55
 
    )
 
52
# TODO: option to show delta summaries for merged-in revisions
 
53
 
 
54
from itertools import izip
56
55
import re
57
 
import sys
58
 
from warnings import (
59
 
    warn,
60
 
    )
61
 
 
62
 
from bzrlib.lazy_import import lazy_import
63
 
lazy_import(globals(), """
64
 
 
65
 
from bzrlib import (
66
 
    config,
67
 
    errors,
68
 
    repository as _mod_repository,
69
 
    revision as _mod_revision,
70
 
    revisionspec,
71
 
    trace,
72
 
    tsort,
73
 
    )
74
 
""")
75
 
 
76
 
from bzrlib import (
77
 
    registry,
78
 
    )
79
 
from bzrlib.osutils import (
80
 
    format_date,
81
 
    get_terminal_encoding,
82
 
    terminal_width,
83
 
    )
 
56
 
 
57
from bzrlib import symbol_versioning
 
58
import bzrlib.errors as errors
 
59
from bzrlib.symbol_versioning import deprecated_method, zero_eleven
 
60
from bzrlib.trace import mutter
 
61
from bzrlib.tsort import merge_sort
84
62
 
85
63
 
86
64
def find_touching_revisions(branch, file_id):
127
105
        revno += 1
128
106
 
129
107
 
 
108
 
130
109
def _enumerate_history(branch):
131
110
    rh = []
132
111
    revno = 1
143
122
             direction='reverse',
144
123
             start_revision=None,
145
124
             end_revision=None,
146
 
             search=None,
147
 
             limit=None):
 
125
             search=None):
148
126
    """Write out human-readable log of commits to this branch.
149
127
 
150
128
    lf
166
144
 
167
145
    end_revision
168
146
        If not None, only show revisions <= end_revision
169
 
 
170
 
    search
171
 
        If not None, only show revisions with matching commit messages
172
 
 
173
 
    limit
174
 
        If not None or 0, only show limit revisions
175
147
    """
176
148
    branch.lock_read()
177
149
    try:
178
 
        if getattr(lf, 'begin_log', None):
179
 
            lf.begin_log()
180
 
 
181
150
        _show_log(branch, lf, specific_fileid, verbose, direction,
182
 
                  start_revision, end_revision, search, limit)
183
 
 
184
 
        if getattr(lf, 'end_log', None):
185
 
            lf.end_log()
 
151
                  start_revision, end_revision, search)
186
152
    finally:
187
153
        branch.unlock()
188
 
 
189
 
 
 
154
    
190
155
def _show_log(branch,
191
156
             lf,
192
157
             specific_fileid=None,
194
159
             direction='reverse',
195
160
             start_revision=None,
196
161
             end_revision=None,
197
 
             search=None,
198
 
             limit=None):
 
162
             search=None):
199
163
    """Worker function for show_log - see show_log."""
 
164
    from bzrlib.osutils import format_date
 
165
    from bzrlib.errors import BzrCheckError
 
166
    
 
167
    from warnings import warn
 
168
 
200
169
    if not isinstance(lf, LogFormatter):
201
170
        warn("not a LogFormatter instance: %r" % lf)
202
171
 
203
172
    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)
 
173
        mutter('get log for file_id %r', specific_fileid)
 
174
 
213
175
    if search is not None:
 
176
        import re
214
177
        searchRE = re.compile(search, re.IGNORECASE)
215
178
    else:
216
179
        searchRE = None
217
180
 
218
 
    rev_tag_dict = {}
219
 
    generate_tags = getattr(lf, 'supports_tags', False)
220
 
    if generate_tags:
221
 
        if branch.supports_tags():
222
 
            rev_tag_dict = branch.tags.get_reverse_tag_dict()
223
 
 
224
 
    generate_delta = verbose and getattr(lf, 'supports_delta', False)
225
 
 
 
181
    which_revs = _enumerate_history(branch)
 
182
    
 
183
    if start_revision is None:
 
184
        start_revision = 1
 
185
    else:
 
186
        branch.check_real_revno(start_revision)
 
187
    
 
188
    if end_revision is None:
 
189
        end_revision = len(which_revs)
 
190
    else:
 
191
        branch.check_real_revno(end_revision)
 
192
 
 
193
    # list indexes are 0-based; revisions are 1-based
 
194
    cut_revs = which_revs[(start_revision-1):(end_revision)]
 
195
    if not cut_revs:
 
196
        return
 
197
 
 
198
    # convert the revision history to a dictionary:
 
199
    rev_nos = dict((k, v) for v, k in cut_revs)
 
200
 
 
201
    # override the mainline to look like the revision history.
 
202
    mainline_revs = [revision_id for index, revision_id in cut_revs]
 
203
    if cut_revs[0][0] == 1:
 
204
        mainline_revs.insert(0, None)
 
205
    else:
 
206
        mainline_revs.insert(0, which_revs[start_revision-2][1])
 
207
    # how should we show merged revisions ?
 
208
    # old api: show_merge. New api: show_merge_revno
 
209
    show_merge_revno = getattr(lf, 'show_merge_revno', None)
 
210
    show_merge = getattr(lf, 'show_merge', None)
 
211
    if show_merge is None and show_merge_revno is None:
 
212
        # no merged-revno support
 
213
        include_merges = False
 
214
    else:
 
215
        include_merges = True
 
216
    if show_merge is not None and show_merge_revno is None:
 
217
        # tell developers to update their code
 
218
        symbol_versioning.warn('LogFormatters should provide show_merge_revno '
 
219
            'instead of show_merge since bzr 0.11.',
 
220
            DeprecationWarning, stacklevel=3)
 
221
    view_revisions = list(get_view_revisions(mainline_revs, rev_nos, branch,
 
222
                          direction, include_merges=include_merges))
 
223
 
 
224
    def iter_revisions():
 
225
        # r = revision, n = revno, d = merge depth
 
226
        revision_ids = [r for r, n, d in view_revisions]
 
227
        zeros = set(r for r, n, d in view_revisions if d == 0)
 
228
        num = 9
 
229
        repository = branch.repository
 
230
        while revision_ids:
 
231
            cur_deltas = {}
 
232
            revisions = repository.get_revisions(revision_ids[:num])
 
233
            if verbose or specific_fileid:
 
234
                delta_revisions = [r for r in revisions if
 
235
                                   r.revision_id in zeros]
 
236
                deltas = repository.get_deltas_for_revisions(delta_revisions)
 
237
                cur_deltas = dict(izip((r.revision_id for r in 
 
238
                                        delta_revisions), deltas))
 
239
            for revision in revisions:
 
240
                # The delta value will be None unless
 
241
                # 1. verbose or specific_fileid is specified, and
 
242
                # 2. the revision is a mainline revision
 
243
                yield revision, cur_deltas.get(revision.revision_id)
 
244
            revision_ids  = revision_ids[num:]
 
245
            num = int(num * 1.5)
 
246
            
226
247
    # now we just print all the revisions
227
 
    log_count = 0
228
 
    for (rev_id, revno, merge_depth), rev, delta in _iter_revisions(
229
 
        branch.repository, view_revisions, generate_delta):
 
248
    for ((rev_id, revno, merge_depth), (rev, delta)) in \
 
249
         izip(view_revisions, iter_revisions()):
 
250
 
230
251
        if searchRE:
231
252
            if not searchRE.search(rev.message):
232
253
                continue
233
254
 
234
 
        lr = LogRevision(rev, revno, merge_depth, delta,
235
 
                         rev_tag_dict.get(rev_id))
236
 
        lf.log_revision(lr)
237
 
        if limit:
238
 
            log_count += 1
239
 
            if log_count >= limit:
240
 
                break
241
 
 
242
 
 
243
 
def calculate_view_revisions(branch, start_revision, end_revision, direction,
244
 
                             specific_fileid, generate_merge_revisions,
245
 
                             allow_single_merge_revision):
246
 
    if (not generate_merge_revisions and start_revision is end_revision is
247
 
        None and direction == 'reverse' and specific_fileid is None):
248
 
        return _linear_view_revisions(branch)
249
 
 
250
 
    mainline_revs, rev_nos, start_rev_id, end_rev_id = \
251
 
        _get_mainline_revs(branch, start_revision, end_revision)
252
 
    if not mainline_revs:
253
 
        return []
254
 
 
255
 
    if direction == 'reverse':
256
 
        start_rev_id, end_rev_id = end_rev_id, start_rev_id
257
 
 
258
 
    generate_single_revision = False
259
 
    if ((not generate_merge_revisions)
260
 
        and ((start_rev_id and (start_rev_id not in rev_nos))
261
 
            or (end_rev_id and (end_rev_id not in rev_nos)))):
262
 
        generate_single_revision = ((start_rev_id == end_rev_id)
263
 
            and allow_single_merge_revision)
264
 
        if not generate_single_revision:
265
 
            raise errors.BzrCommandError('Selected log formatter only supports'
266
 
                ' mainline revisions.')
267
 
        generate_merge_revisions = generate_single_revision
268
 
    view_revs_iter = get_view_revisions(mainline_revs, rev_nos, branch,
269
 
                          direction, include_merges=generate_merge_revisions)
270
 
    view_revisions = _filter_revision_range(list(view_revs_iter),
271
 
                                            start_rev_id,
272
 
                                            end_rev_id)
273
 
    if view_revisions and generate_single_revision:
274
 
        view_revisions = view_revisions[0:1]
275
 
    if specific_fileid:
276
 
        view_revisions = _filter_revisions_touching_file_id(branch,
277
 
                                                         specific_fileid,
278
 
                                                         mainline_revs,
279
 
                                                         view_revisions)
280
 
 
281
 
    # rebase merge_depth - unless there are no revisions or 
282
 
    # either the first or last revision have merge_depth = 0.
283
 
    if view_revisions and view_revisions[0][2] and view_revisions[-1][2]:
284
 
        min_depth = min([d for r,n,d in view_revisions])
285
 
        if min_depth != 0:
286
 
            view_revisions = [(r,n,d-min_depth) for r,n,d in view_revisions]
287
 
    return view_revisions
288
 
 
289
 
 
290
 
def _linear_view_revisions(branch):
291
 
    start_revno, start_revision_id = branch.last_revision_info()
292
 
    repo = branch.repository
293
 
    revision_ids = repo.iter_reverse_revision_history(start_revision_id)
294
 
    for num, revision_id in enumerate(revision_ids):
295
 
        yield revision_id, str(start_revno - num), 0
296
 
 
297
 
 
298
 
def _iter_revisions(repository, view_revisions, generate_delta):
299
 
    num = 9
300
 
    view_revisions = iter(view_revisions)
301
 
    while True:
302
 
        cur_view_revisions = [d for x, d in zip(range(num), view_revisions)]
303
 
        if len(cur_view_revisions) == 0:
304
 
            break
305
 
        cur_deltas = {}
306
 
        # r = revision, n = revno, d = merge depth
307
 
        revision_ids = [r for (r, n, d) in cur_view_revisions]
308
 
        revisions = repository.get_revisions(revision_ids)
309
 
        if generate_delta:
310
 
            deltas = repository.get_deltas_for_revisions(revisions)
311
 
            cur_deltas = dict(izip((r.revision_id for r in revisions),
312
 
                                   deltas))
313
 
        for view_data, revision in izip(cur_view_revisions, revisions):
314
 
            yield view_data, revision, cur_deltas.get(revision.revision_id)
315
 
        num = min(int(num * 1.5), 200)
316
 
 
317
 
 
318
 
def _get_mainline_revs(branch, start_revision, end_revision):
319
 
    """Get the mainline revisions from the branch.
320
 
    
321
 
    Generates the list of mainline revisions for the branch.
322
 
    
323
 
    :param  branch: The branch containing the revisions. 
324
 
 
325
 
    :param  start_revision: The first revision to be logged.
326
 
            For backwards compatibility this may be a mainline integer revno,
327
 
            but for merge revision support a RevisionInfo is expected.
328
 
 
329
 
    :param  end_revision: The last revision to be logged.
330
 
            For backwards compatibility this may be a mainline integer revno,
331
 
            but for merge revision support a RevisionInfo is expected.
332
 
 
333
 
    :return: A (mainline_revs, rev_nos, start_rev_id, end_rev_id) tuple.
334
 
    """
335
 
    branch_revno, branch_last_revision = branch.last_revision_info()
336
 
    if branch_revno == 0:
337
 
        return None, None, None, None
338
 
 
339
 
    # For mainline generation, map start_revision and end_revision to 
340
 
    # mainline revnos. If the revision is not on the mainline choose the 
341
 
    # appropriate extreme of the mainline instead - the extra will be 
342
 
    # filtered later.
343
 
    # Also map the revisions to rev_ids, to be used in the later filtering
344
 
    # stage.
345
 
    start_rev_id = None 
346
 
    if start_revision is None:
347
 
        start_revno = 1
348
 
    else:
349
 
        if isinstance(start_revision, revisionspec.RevisionInfo):
350
 
            start_rev_id = start_revision.rev_id
351
 
            start_revno = start_revision.revno or 1
352
 
        else:
353
 
            branch.check_real_revno(start_revision)
354
 
            start_revno = start_revision
355
 
    
356
 
    end_rev_id = None
357
 
    if end_revision is None:
358
 
        end_revno = branch_revno
359
 
    else:
360
 
        if isinstance(end_revision, revisionspec.RevisionInfo):
361
 
            end_rev_id = end_revision.rev_id
362
 
            end_revno = end_revision.revno or branch_revno
363
 
        else:
364
 
            branch.check_real_revno(end_revision)
365
 
            end_revno = end_revision
366
 
 
367
 
    if ((start_rev_id == _mod_revision.NULL_REVISION)
368
 
        or (end_rev_id == _mod_revision.NULL_REVISION)):
369
 
        raise errors.BzrCommandError('Logging revision 0 is invalid.')
370
 
    if start_revno > end_revno:
371
 
        raise errors.BzrCommandError("Start revision must be older than "
372
 
                                     "the end revision.")
373
 
 
374
 
    if end_revno < start_revno:
375
 
        return None, None, None, None
376
 
    cur_revno = branch_revno
377
 
    rev_nos = {}
378
 
    mainline_revs = []
379
 
    for revision_id in branch.repository.iter_reverse_revision_history(
380
 
                        branch_last_revision):
381
 
        if cur_revno < start_revno:
382
 
            # We have gone far enough, but we always add 1 more revision
383
 
            rev_nos[revision_id] = cur_revno
384
 
            mainline_revs.append(revision_id)
385
 
            break
386
 
        if cur_revno <= end_revno:
387
 
            rev_nos[revision_id] = cur_revno
388
 
            mainline_revs.append(revision_id)
389
 
        cur_revno -= 1
390
 
    else:
391
 
        # We walked off the edge of all revisions, so we add a 'None' marker
392
 
        mainline_revs.append(None)
393
 
 
394
 
    mainline_revs.reverse()
395
 
 
396
 
    # override the mainline to look like the revision history.
397
 
    return mainline_revs, rev_nos, start_rev_id, end_rev_id
398
 
 
399
 
 
400
 
def _filter_revision_range(view_revisions, start_rev_id, end_rev_id):
401
 
    """Filter view_revisions based on revision ranges.
402
 
 
403
 
    :param view_revisions: A list of (revision_id, dotted_revno, merge_depth) 
404
 
            tuples to be filtered.
405
 
 
406
 
    :param start_rev_id: If not NONE specifies the first revision to be logged.
407
 
            If NONE then all revisions up to the end_rev_id are logged.
408
 
 
409
 
    :param end_rev_id: If not NONE specifies the last revision to be logged.
410
 
            If NONE then all revisions up to the end of the log are logged.
411
 
 
412
 
    :return: The filtered view_revisions.
413
 
    """
414
 
    if start_rev_id or end_rev_id: 
415
 
        revision_ids = [r for r, n, d in view_revisions]
416
 
        if start_rev_id:
417
 
            start_index = revision_ids.index(start_rev_id)
418
 
        else:
419
 
            start_index = 0
420
 
        if start_rev_id == end_rev_id:
421
 
            end_index = start_index
422
 
        else:
423
 
            if end_rev_id:
424
 
                end_index = revision_ids.index(end_rev_id)
 
255
        if merge_depth == 0:
 
256
            # a mainline revision.
 
257
                
 
258
            if specific_fileid:
 
259
                if not delta.touches_file_id(specific_fileid):
 
260
                    continue
 
261
    
 
262
            if not verbose:
 
263
                # although we calculated it, throw it away without display
 
264
                delta = None
 
265
 
 
266
            lf.show(revno, rev, delta)
 
267
        else:
 
268
            if show_merge_revno is None:
 
269
                lf.show_merge(rev, merge_depth)
425
270
            else:
426
 
                end_index = len(view_revisions) - 1
427
 
        # To include the revisions merged into the last revision, 
428
 
        # extend end_rev_id down to, but not including, the next rev
429
 
        # with the same or lesser merge_depth
430
 
        end_merge_depth = view_revisions[end_index][2]
431
 
        try:
432
 
            for index in xrange(end_index+1, len(view_revisions)+1):
433
 
                if view_revisions[index][2] <= end_merge_depth:
434
 
                    end_index = index - 1
435
 
                    break
436
 
        except IndexError:
437
 
            # if the search falls off the end then log to the end as well
438
 
            end_index = len(view_revisions) - 1
439
 
        view_revisions = view_revisions[start_index:end_index+1]
440
 
    return view_revisions
441
 
 
442
 
 
443
 
def _filter_revisions_touching_file_id(branch, file_id, mainline_revisions,
444
 
                                       view_revs_iter):
445
 
    """Return the list of revision ids which touch a given file id.
446
 
 
447
 
    The function filters view_revisions and returns a subset.
448
 
    This includes the revisions which directly change the file id,
449
 
    and the revisions which merge these changes. So if the
450
 
    revision graph is::
451
 
        A
452
 
        |\
453
 
        B C
454
 
        |/
455
 
        D
456
 
 
457
 
    And 'C' changes a file, then both C and D will be returned.
458
 
 
459
 
    This will also can be restricted based on a subset of the mainline.
460
 
 
461
 
    :return: A list of (revision_id, dotted_revno, merge_depth) tuples.
462
 
    """
463
 
    # find all the revisions that change the specific file
464
 
    # build the ancestry of each revision in the graph
465
 
    # - only listing the ancestors that change the specific file.
466
 
    graph = branch.repository.get_graph()
467
 
    # This asks for all mainline revisions, which means we only have to spider
468
 
    # sideways, rather than depth history. That said, its still size-of-history
469
 
    # and should be addressed.
470
 
    # mainline_revisions always includes an extra revision at the beginning, so
471
 
    # don't request it.
472
 
    parent_map = dict(((key, value) for key, value in
473
 
        graph.iter_ancestry(mainline_revisions[1:]) if value is not None))
474
 
    sorted_rev_list = tsort.topo_sort(parent_map.items())
475
 
    text_keys = [(file_id, rev_id) for rev_id in sorted_rev_list]
476
 
    modified_text_versions = branch.repository.texts.get_parent_map(text_keys)
477
 
    ancestry = {}
478
 
    for rev in sorted_rev_list:
479
 
        text_key = (file_id, rev)
480
 
        parents = parent_map[rev]
481
 
        if text_key not in modified_text_versions and len(parents) == 1:
482
 
            # We will not be adding anything new, so just use a reference to
483
 
            # the parent ancestry.
484
 
            rev_ancestry = ancestry[parents[0]]
485
 
        else:
486
 
            rev_ancestry = set()
487
 
            if text_key in modified_text_versions:
488
 
                rev_ancestry.add(rev)
489
 
            for parent in parents:
490
 
                if parent not in ancestry:
491
 
                    # parent is a Ghost, which won't be present in
492
 
                    # sorted_rev_list, but we may access it later, so create an
493
 
                    # empty node for it
494
 
                    ancestry[parent] = set()
495
 
                rev_ancestry = rev_ancestry.union(ancestry[parent])
496
 
        ancestry[rev] = rev_ancestry
497
 
 
498
 
    def is_merging_rev(r):
499
 
        parents = parent_map[r]
500
 
        if len(parents) > 1:
501
 
            leftparent = parents[0]
502
 
            for rightparent in parents[1:]:
503
 
                if not ancestry[leftparent].issuperset(
504
 
                        ancestry[rightparent]):
505
 
                    return True
506
 
        return False
507
 
 
508
 
    # filter from the view the revisions that did not change or merge 
509
 
    # the specific file
510
 
    return [(r, n, d) for r, n, d in view_revs_iter
511
 
            if (file_id, r) in modified_text_versions or is_merging_rev(r)]
 
271
                lf.show_merge_revno(rev, merge_depth, revno)
512
272
 
513
273
 
514
274
def get_view_revisions(mainline_revs, rev_nos, branch, direction,
524
284
        for revision_id in revision_ids:
525
285
            yield revision_id, str(rev_nos[revision_id]), 0
526
286
        return
527
 
    graph = branch.repository.get_graph()
528
 
    # This asks for all mainline revisions, which means we only have to spider
529
 
    # sideways, rather than depth history. That said, its still size-of-history
530
 
    # and should be addressed.
531
 
    # mainline_revisions always includes an extra revision at the beginning, so
532
 
    # don't request it.
533
 
    parent_map = dict(((key, value) for key, value in
534
 
        graph.iter_ancestry(mainline_revs[1:]) if value is not None))
535
 
    # filter out ghosts; merge_sort errors on ghosts.
536
 
    rev_graph = _mod_repository._strip_NULL_ghosts(parent_map)
537
 
    merge_sorted_revisions = tsort.merge_sort(
538
 
        rev_graph,
 
287
    merge_sorted_revisions = merge_sort(
 
288
        branch.repository.get_revision_graph(mainline_revs[-1]),
539
289
        mainline_revs[-1],
540
290
        mainline_revs,
541
291
        generate_revno=True)
562
312
        if val[2] == _depth:
563
313
            zd_revisions.append([val])
564
314
        else:
 
315
            assert val[2] > _depth
565
316
            zd_revisions[-1].append(val)
566
317
    for revisions in zd_revisions:
567
318
        if len(revisions) > 1:
573
324
    return result
574
325
 
575
326
 
576
 
class LogRevision(object):
577
 
    """A revision to be logged (by LogFormatter.log_revision).
578
 
 
579
 
    A simple wrapper for the attributes of a revision to be logged.
580
 
    The attributes may or may not be populated, as determined by the 
581
 
    logging options and the log formatter capabilities.
582
 
    """
583
 
 
584
 
    def __init__(self, rev=None, revno=None, merge_depth=0, delta=None,
585
 
                 tags=None):
586
 
        self.rev = rev
587
 
        self.revno = revno
588
 
        self.merge_depth = merge_depth
589
 
        self.delta = delta
590
 
        self.tags = tags
591
 
 
592
 
 
593
327
class LogFormatter(object):
594
 
    """Abstract class to display log messages.
595
 
 
596
 
    At a minimum, a derived class must implement the log_revision method.
597
 
 
598
 
    If the LogFormatter needs to be informed of the beginning or end of
599
 
    a log it should implement the begin_log and/or end_log hook methods.
600
 
 
601
 
    A LogFormatter should define the following supports_XXX flags 
602
 
    to indicate which LogRevision attributes it supports:
603
 
 
604
 
    - supports_delta must be True if this log formatter supports delta.
605
 
        Otherwise the delta attribute may not be populated.
606
 
    - supports_merge_revisions must be True if this log formatter supports 
607
 
        merge revisions.  If not, and if supports_single_merge_revisions is
608
 
        also not True, then only mainline revisions will be passed to the 
609
 
        formatter.
610
 
    - supports_single_merge_revision must be True if this log formatter
611
 
        supports logging only a single merge revision.  This flag is
612
 
        only relevant if supports_merge_revisions is not True.
613
 
    - supports_tags must be True if this log formatter supports tags.
614
 
        Otherwise the tags attribute may not be populated.
615
 
 
616
 
    Plugins can register functions to show custom revision properties using
617
 
    the properties_handler_registry. The registered function
618
 
    must respect the following interface description:
619
 
        def my_show_properties(properties_dict):
620
 
            # code that returns a dict {'name':'value'} of the properties 
621
 
            # to be shown
622
 
    """
 
328
    """Abstract class to display log messages."""
623
329
 
624
330
    def __init__(self, to_file, show_ids=False, show_timezone='original'):
625
331
        self.to_file = to_file
626
332
        self.show_ids = show_ids
627
333
        self.show_timezone = show_timezone
628
334
 
629
 
# TODO: uncomment this block after show() has been removed.
630
 
# Until then defining log_revision would prevent _show_log calling show() 
631
 
# in legacy formatters.
632
 
#    def log_revision(self, revision):
633
 
#        """Log a revision.
634
 
#
635
 
#        :param  revision:   The LogRevision to be logged.
636
 
#        """
637
 
#        raise NotImplementedError('not implemented in abstract base')
 
335
    def show(self, revno, rev, delta):
 
336
        raise NotImplementedError('not implemented in abstract base')
638
337
 
639
338
    def short_committer(self, rev):
640
 
        name, address = config.parse_username(rev.committer)
641
 
        if name:
642
 
            return name
643
 
        return address
644
 
 
645
 
    def short_author(self, rev):
646
 
        name, address = config.parse_username(rev.get_apparent_author())
647
 
        if name:
648
 
            return name
649
 
        return address
650
 
 
651
 
    def show_properties(self, revision, indent):
652
 
        """Displays the custom properties returned by each registered handler.
653
 
        
654
 
        If a registered handler raises an error it is propagated.
655
 
        """
656
 
        for key, handler in properties_handler_registry.iteritems():
657
 
            for key, value in handler(revision).items():
658
 
                self.to_file.write(indent + key + ': ' + value + '\n')
659
 
 
660
 
 
 
339
        return re.sub('<.*@.*>', '', rev.committer).strip(' ')
 
340
    
 
341
    
661
342
class LongLogFormatter(LogFormatter):
662
 
 
663
 
    supports_merge_revisions = True
664
 
    supports_delta = True
665
 
    supports_tags = True
666
 
 
667
 
    def log_revision(self, revision):
668
 
        """Log a revision, either merged or not."""
669
 
        indent = '    ' * revision.merge_depth
 
343
    def show(self, revno, rev, delta):
 
344
        return self._show_helper(revno=revno, rev=rev, delta=delta)
 
345
 
 
346
    @deprecated_method(zero_eleven)
 
347
    def show_merge(self, rev, merge_depth):
 
348
        return self._show_helper(rev=rev, indent='    '*merge_depth, merged=True, delta=None)
 
349
 
 
350
    def show_merge_revno(self, rev, merge_depth, revno):
 
351
        """Show a merged revision rev, with merge_depth and a revno."""
 
352
        return self._show_helper(rev=rev, revno=revno,
 
353
            indent='    '*merge_depth, merged=True, delta=None)
 
354
 
 
355
    def _show_helper(self, rev=None, revno=None, indent='', merged=False, delta=None):
 
356
        """Show a revision, either merged or not."""
 
357
        from bzrlib.osutils import format_date
670
358
        to_file = self.to_file
671
 
        to_file.write(indent + '-' * 60 + '\n')
672
 
        if revision.revno is not None:
673
 
            to_file.write(indent + 'revno: %s\n' % (revision.revno,))
674
 
        if revision.tags:
675
 
            to_file.write(indent + 'tags: %s\n' % (', '.join(revision.tags)))
 
359
        print >>to_file,  indent+'-' * 60
 
360
        if revno is not None:
 
361
            print >>to_file,  indent+'revno:', revno
 
362
        if merged:
 
363
            print >>to_file,  indent+'merged:', rev.revision_id
 
364
        elif self.show_ids:
 
365
            print >>to_file,  indent+'revision-id:', rev.revision_id
676
366
        if self.show_ids:
677
 
            to_file.write(indent + 'revision-id: ' + revision.rev.revision_id)
678
 
            to_file.write('\n')
679
 
            for parent_id in revision.rev.parent_ids:
680
 
                to_file.write(indent + 'parent: %s\n' % (parent_id,))
681
 
        self.show_properties(revision.rev, indent)
682
 
 
683
 
        author = revision.rev.properties.get('author', None)
684
 
        if author is not None:
685
 
            to_file.write(indent + 'author: %s\n' % (author,))
686
 
        to_file.write(indent + 'committer: %s\n' % (revision.rev.committer,))
687
 
 
688
 
        branch_nick = revision.rev.properties.get('branch-nick', None)
689
 
        if branch_nick is not None:
690
 
            to_file.write(indent + 'branch nick: %s\n' % (branch_nick,))
691
 
 
692
 
        date_str = format_date(revision.rev.timestamp,
693
 
                               revision.rev.timezone or 0,
 
367
            for parent_id in rev.parent_ids:
 
368
                print >>to_file, indent+'parent:', parent_id
 
369
        print >>to_file,  indent+'committer:', rev.committer
 
370
        try:
 
371
            print >>to_file, indent+'branch nick: %s' % \
 
372
                rev.properties['branch-nick']
 
373
        except KeyError:
 
374
            pass
 
375
        date_str = format_date(rev.timestamp,
 
376
                               rev.timezone or 0,
694
377
                               self.show_timezone)
695
 
        to_file.write(indent + 'timestamp: %s\n' % (date_str,))
 
378
        print >>to_file,  indent+'timestamp: %s' % date_str
696
379
 
697
 
        to_file.write(indent + 'message:\n')
698
 
        if not revision.rev.message:
699
 
            to_file.write(indent + '  (no message)\n')
 
380
        print >>to_file,  indent+'message:'
 
381
        if not rev.message:
 
382
            print >>to_file,  indent+'  (no message)'
700
383
        else:
701
 
            message = revision.rev.message.rstrip('\r\n')
 
384
            message = rev.message.rstrip('\r\n')
702
385
            for l in message.split('\n'):
703
 
                to_file.write(indent + '  %s\n' % (l,))
704
 
        if revision.delta is not None:
705
 
            revision.delta.show(to_file, self.show_ids, indent=indent)
 
386
                print >>to_file,  indent+'  ' + l
 
387
        if delta is not None:
 
388
            delta.show(to_file, self.show_ids)
706
389
 
707
390
 
708
391
class ShortLogFormatter(LogFormatter):
709
 
 
710
 
    supports_delta = True
711
 
    supports_single_merge_revision = True
712
 
 
713
 
    def log_revision(self, revision):
 
392
    def show(self, revno, rev, delta):
 
393
        from bzrlib.osutils import format_date
 
394
 
714
395
        to_file = self.to_file
715
 
        is_merge = ''
716
 
        if len(revision.rev.parent_ids) > 1:
717
 
            is_merge = ' [merge]'
718
 
        to_file.write("%5s %s\t%s%s\n" % (revision.revno,
719
 
                self.short_author(revision.rev),
720
 
                format_date(revision.rev.timestamp,
721
 
                            revision.rev.timezone or 0,
 
396
        date_str = format_date(rev.timestamp, rev.timezone or 0,
 
397
                            self.show_timezone)
 
398
        print >>to_file, "%5s %s\t%s" % (revno, self.short_committer(rev),
 
399
                format_date(rev.timestamp, rev.timezone or 0,
722
400
                            self.show_timezone, date_fmt="%Y-%m-%d",
723
 
                            show_offset=False),
724
 
                is_merge))
 
401
                           show_offset=False))
725
402
        if self.show_ids:
726
 
            to_file.write('      revision-id:%s\n' % (revision.rev.revision_id,))
727
 
        if not revision.rev.message:
728
 
            to_file.write('      (no message)\n')
 
403
            print >>to_file,  '      revision-id:', rev.revision_id
 
404
        if not rev.message:
 
405
            print >>to_file,  '      (no message)'
729
406
        else:
730
 
            message = revision.rev.message.rstrip('\r\n')
 
407
            message = rev.message.rstrip('\r\n')
731
408
            for l in message.split('\n'):
732
 
                to_file.write('      %s\n' % (l,))
 
409
                print >>to_file,  '      ' + l
733
410
 
734
411
        # TODO: Why not show the modified files in a shorter form as
735
412
        # well? rewrap them single lines of appropriate length
736
 
        if revision.delta is not None:
737
 
            revision.delta.show(to_file, self.show_ids)
738
 
        to_file.write('\n')
 
413
        if delta is not None:
 
414
            delta.show(to_file, self.show_ids)
 
415
        print >>to_file, ''
739
416
 
740
417
 
741
418
class LineLogFormatter(LogFormatter):
742
 
 
743
 
    supports_single_merge_revision = True
744
 
 
745
 
    def __init__(self, *args, **kwargs):
746
 
        super(LineLogFormatter, self).__init__(*args, **kwargs)
747
 
        self._max_chars = terminal_width() - 1
748
 
 
749
419
    def truncate(self, str, max_len):
750
420
        if len(str) <= max_len:
751
421
            return str
752
422
        return str[:max_len-3]+'...'
753
423
 
754
424
    def date_string(self, rev):
 
425
        from bzrlib.osutils import format_date
755
426
        return format_date(rev.timestamp, rev.timezone or 0, 
756
427
                           self.show_timezone, date_fmt="%Y-%m-%d",
757
428
                           show_offset=False)
762
433
        else:
763
434
            return rev.message
764
435
 
765
 
    def log_revision(self, revision):
766
 
        self.to_file.write(self.log_string(revision.revno, revision.rev,
767
 
                                              self._max_chars))
768
 
        self.to_file.write('\n')
 
436
    def show(self, revno, rev, delta):
 
437
        from bzrlib.osutils import terminal_width
 
438
        print >> self.to_file, self.log_string(revno, rev, terminal_width()-1)
769
439
 
770
440
    def log_string(self, revno, rev, max_chars):
771
441
        """Format log info into one string. Truncate tail of string
779
449
        if revno:
780
450
            # show revno only when is not None
781
451
            out.append("%s:" % revno)
782
 
        out.append(self.truncate(self.short_author(rev), 20))
 
452
        out.append(self.truncate(self.short_committer(rev), 20))
783
453
        out.append(self.date_string(rev))
784
454
        out.append(rev.get_summary())
785
455
        return self.truncate(" ".join(out).rstrip('\n'), max_chars)
789
459
    lf = LineLogFormatter(None)
790
460
    return lf.log_string(None, rev, max_chars)
791
461
 
792
 
 
793
 
class LogFormatterRegistry(registry.Registry):
794
 
    """Registry for log formatters"""
795
 
 
796
 
    def make_formatter(self, name, *args, **kwargs):
797
 
        """Construct a formatter from arguments.
798
 
 
799
 
        :param name: Name of the formatter to construct.  'short', 'long' and
800
 
            'line' are built-in.
801
 
        """
802
 
        return self.get(name)(*args, **kwargs)
803
 
 
804
 
    def get_default(self, branch):
805
 
        return self.get(branch.get_config().log_format())
806
 
 
807
 
 
808
 
log_formatter_registry = LogFormatterRegistry()
809
 
 
810
 
 
811
 
log_formatter_registry.register('short', ShortLogFormatter,
812
 
                                'Moderately short log format')
813
 
log_formatter_registry.register('long', LongLogFormatter,
814
 
                                'Detailed log format')
815
 
log_formatter_registry.register('line', LineLogFormatter,
816
 
                                'Log format with one line per revision')
817
 
 
 
462
FORMATTERS = {
 
463
              'long': LongLogFormatter,
 
464
              'short': ShortLogFormatter,
 
465
              'line': LineLogFormatter,
 
466
              }
818
467
 
819
468
def register_formatter(name, formatter):
820
 
    log_formatter_registry.register(name, formatter)
821
 
 
 
469
    FORMATTERS[name] = formatter
822
470
 
823
471
def log_formatter(name, *args, **kwargs):
824
472
    """Construct a formatter from arguments.
826
474
    name -- Name of the formatter to construct; currently 'long', 'short' and
827
475
        'line' are supported.
828
476
    """
 
477
    from bzrlib.errors import BzrCommandError
829
478
    try:
830
 
        return log_formatter_registry.make_formatter(name, *args, **kwargs)
 
479
        return FORMATTERS[name](*args, **kwargs)
831
480
    except KeyError:
832
 
        raise errors.BzrCommandError("unknown log formatter: %r" % name)
833
 
 
 
481
        raise BzrCommandError("unknown log formatter: %r" % name)
834
482
 
835
483
def show_one_log(revno, rev, delta, verbose, to_file, show_timezone):
836
484
    # deprecated; for compatibility
837
485
    lf = LongLogFormatter(to_file=to_file, show_timezone=show_timezone)
838
486
    lf.show(revno, rev, delta)
839
487
 
840
 
 
841
 
def show_changed_revisions(branch, old_rh, new_rh, to_file=None,
842
 
                           log_format='long'):
 
488
def show_changed_revisions(branch, old_rh, new_rh, to_file=None, log_format='long'):
843
489
    """Show the change in revision history comparing the old revision history to the new one.
844
490
 
845
491
    :param branch: The branch where the revisions exist
848
494
    :param to_file: A file to write the results to. If None, stdout will be used
849
495
    """
850
496
    if to_file is None:
851
 
        to_file = codecs.getwriter(get_terminal_encoding())(sys.stdout,
852
 
            errors='replace')
 
497
        import sys
 
498
        import codecs
 
499
        import bzrlib
 
500
        to_file = codecs.getwriter(bzrlib.user_encoding)(sys.stdout, errors='replace')
853
501
    lf = log_formatter(log_format,
854
502
                       show_ids=False,
855
503
                       to_file=to_file,
877
525
        to_file.write('\nRemoved Revisions:\n')
878
526
        for i in range(base_idx, len(old_rh)):
879
527
            rev = branch.repository.get_revision(old_rh[i])
880
 
            lr = LogRevision(rev, i+1, 0, None)
881
 
            lf.log_revision(lr)
 
528
            lf.show(i+1, rev, None)
882
529
        to_file.write('*'*60)
883
530
        to_file.write('\n\n')
884
531
    if base_idx < len(new_rh):
886
533
        show_log(branch,
887
534
                 lf,
888
535
                 None,
889
 
                 verbose=False,
 
536
                 verbose=True,
890
537
                 direction='forward',
891
538
                 start_revision=base_idx+1,
892
539
                 end_revision=len(new_rh),
893
540
                 search=None)
894
541
 
895
 
 
896
 
properties_handler_registry = registry.Registry()