~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/log.py

  • Committer: Aaron Bentley
  • Date: 2006-04-19 02:08:51 UTC
  • mto: This revision was merged to the branch mainline in revision 1672.
  • Revision ID: aaron.bentley@utoronto.ca-20060419020851-830a65e114637604
Clarifications from merge review

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2005, 2006, 2007 Canonical Ltd
2
 
#
 
1
# Copyright (C) 2005 Canonical Ltd
 
2
 
3
3
# This program is free software; you can redistribute it and/or modify
4
4
# it under the terms of the GNU General Public License as published by
5
5
# the Free Software Foundation; either version 2 of the License, or
6
6
# (at your option) any later version.
7
 
#
 
7
 
8
8
# This program is distributed in the hope that it will be useful,
9
9
# but WITHOUT ANY WARRANTY; without even the implied warranty of
10
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11
11
# GNU General Public License for more details.
12
 
#
 
12
 
13
13
# You should have received a copy of the GNU General Public License
14
14
# along with this program; if not, write to the Free Software
15
15
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
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
 
 
53
# TODO: option to show delta summaries for merged-in revisions
56
54
import re
57
 
import sys
58
 
from warnings import (
59
 
    warn,
60
 
    )
61
55
 
62
 
from bzrlib import (
63
 
    config,
64
 
    lazy_regex,
65
 
    registry,
66
 
    )
67
 
from bzrlib.errors import (
68
 
    BzrCommandError,
69
 
    )
70
 
from bzrlib.osutils import (
71
 
    format_date,
72
 
    get_terminal_encoding,
73
 
    terminal_width,
74
 
    )
75
 
from bzrlib.repository import _strip_NULL_ghosts
76
 
from bzrlib.revision import (
77
 
    NULL_REVISION,
78
 
    )
79
 
from bzrlib.revisionspec import (
80
 
    RevisionInfo,
81
 
    )
 
56
from bzrlib.delta import compare_trees
 
57
import bzrlib.errors as errors
82
58
from bzrlib.trace import mutter
83
 
from bzrlib.tsort import (
84
 
    merge_sort,
85
 
    topo_sort,
86
 
    )
 
59
from bzrlib.tree import EmptyTree
 
60
from bzrlib.tsort import merge_sort
87
61
 
88
62
 
89
63
def find_touching_revisions(branch, file_id):
130
104
        revno += 1
131
105
 
132
106
 
 
107
 
133
108
def _enumerate_history(branch):
134
109
    rh = []
135
110
    revno = 1
139
114
    return rh
140
115
 
141
116
 
 
117
def _get_revision_delta(branch, revno):
 
118
    """Return the delta for a mainline revision.
 
119
    
 
120
    This is used to show summaries in verbose logs, and also for finding 
 
121
    revisions which touch a given file."""
 
122
    # XXX: What are we supposed to do when showing a summary for something 
 
123
    # other than a mainline revision.  The delta to it's first parent, or
 
124
    # (more useful) the delta to a nominated other revision.
 
125
    return branch.get_revision_delta(revno)
 
126
 
 
127
 
142
128
def show_log(branch,
143
129
             lf,
144
130
             specific_fileid=None,
146
132
             direction='reverse',
147
133
             start_revision=None,
148
134
             end_revision=None,
149
 
             search=None,
150
 
             limit=None):
 
135
             search=None):
151
136
    """Write out human-readable log of commits to this branch.
152
137
 
153
138
    lf
169
154
 
170
155
    end_revision
171
156
        If not None, only show revisions <= end_revision
172
 
 
173
 
    search
174
 
        If not None, only show revisions with matching commit messages
175
 
 
176
 
    limit
177
 
        If not None or 0, only show limit revisions
178
157
    """
179
158
    branch.lock_read()
180
159
    try:
181
 
        if getattr(lf, 'begin_log', None):
182
 
            lf.begin_log()
183
 
 
184
160
        _show_log(branch, lf, specific_fileid, verbose, direction,
185
 
                  start_revision, end_revision, search, limit)
186
 
 
187
 
        if getattr(lf, 'end_log', None):
188
 
            lf.end_log()
 
161
                  start_revision, end_revision, search)
189
162
    finally:
190
163
        branch.unlock()
191
 
 
192
 
 
 
164
    
193
165
def _show_log(branch,
194
166
             lf,
195
167
             specific_fileid=None,
197
169
             direction='reverse',
198
170
             start_revision=None,
199
171
             end_revision=None,
200
 
             search=None,
201
 
             limit=None):
 
172
             search=None):
202
173
    """Worker function for show_log - see show_log."""
 
174
    from bzrlib.osutils import format_date
 
175
    from bzrlib.errors import BzrCheckError
 
176
    from bzrlib.textui import show_status
 
177
    
 
178
    from warnings import warn
 
179
 
203
180
    if not isinstance(lf, LogFormatter):
204
181
        warn("not a LogFormatter instance: %r" % lf)
205
182
 
206
183
    if specific_fileid:
207
184
        mutter('get log for file_id %r', specific_fileid)
208
 
    generate_merge_revisions = getattr(lf, 'supports_merge_revisions', False)
209
 
    allow_single_merge_revision = getattr(lf,
210
 
        'supports_single_merge_revision', False)
211
 
    view_revisions = calculate_view_revisions(branch, start_revision,
212
 
                                              end_revision, direction,
213
 
                                              specific_fileid,
214
 
                                              generate_merge_revisions,
215
 
                                              allow_single_merge_revision)
 
185
 
216
186
    if search is not None:
 
187
        import re
217
188
        searchRE = re.compile(search, re.IGNORECASE)
218
189
    else:
219
190
        searchRE = None
220
191
 
221
 
    rev_tag_dict = {}
222
 
    generate_tags = getattr(lf, 'supports_tags', False)
223
 
    if generate_tags:
224
 
        if branch.supports_tags():
225
 
            rev_tag_dict = branch.tags.get_reverse_tag_dict()
226
 
 
227
 
    generate_delta = verbose and getattr(lf, 'supports_delta', False)
228
 
 
229
 
    # now we just print all the revisions
230
 
    log_count = 0
231
 
    for (rev_id, revno, merge_depth), rev, delta in _iter_revisions(
232
 
        branch.repository, view_revisions, generate_delta):
233
 
        if searchRE:
234
 
            if not searchRE.search(rev.message):
235
 
                continue
236
 
 
237
 
        lr = LogRevision(rev, revno, merge_depth, delta,
238
 
                         rev_tag_dict.get(rev_id))
239
 
        lf.log_revision(lr)
240
 
        if limit:
241
 
            log_count += 1
242
 
            if log_count >= limit:
243
 
                break
244
 
 
245
 
 
246
 
def calculate_view_revisions(branch, start_revision, end_revision, direction,
247
 
                             specific_fileid, generate_merge_revisions,
248
 
                             allow_single_merge_revision):
249
 
    if (not generate_merge_revisions and start_revision is end_revision is
250
 
        None and direction == 'reverse' and specific_fileid is None):
251
 
        return _linear_view_revisions(branch)
252
 
 
253
 
    mainline_revs, rev_nos, start_rev_id, end_rev_id = \
254
 
        _get_mainline_revs(branch, start_revision, end_revision)
255
 
    if not mainline_revs:
256
 
        return []
257
 
 
258
 
    if direction == 'reverse':
259
 
        start_rev_id, end_rev_id = end_rev_id, start_rev_id
260
 
 
261
 
    generate_single_revision = False
262
 
    if ((not generate_merge_revisions)
263
 
        and ((start_rev_id and (start_rev_id not in rev_nos))
264
 
            or (end_rev_id and (end_rev_id not in rev_nos)))):
265
 
        generate_single_revision = ((start_rev_id == end_rev_id)
266
 
            and allow_single_merge_revision)
267
 
        if not generate_single_revision:
268
 
            raise BzrCommandError('Selected log formatter only supports '
269
 
                'mainline revisions.')
270
 
        generate_merge_revisions = generate_single_revision
271
 
    view_revs_iter = get_view_revisions(mainline_revs, rev_nos, branch,
272
 
                          direction, include_merges=generate_merge_revisions)
273
 
    view_revisions = _filter_revision_range(list(view_revs_iter),
274
 
                                            start_rev_id,
275
 
                                            end_rev_id)
276
 
    if view_revisions and generate_single_revision:
277
 
        view_revisions = view_revisions[0:1]
278
 
    if specific_fileid:
279
 
        view_revisions = _filter_revisions_touching_file_id(branch,
280
 
                                                         specific_fileid,
281
 
                                                         mainline_revs,
282
 
                                                         view_revisions)
283
 
 
284
 
    # rebase merge_depth - unless there are no revisions or 
285
 
    # either the first or last revision have merge_depth = 0.
286
 
    if view_revisions and view_revisions[0][2] and view_revisions[-1][2]:
287
 
        min_depth = min([d for r,n,d in view_revisions])
288
 
        if min_depth != 0:
289
 
            view_revisions = [(r,n,d-min_depth) for r,n,d in view_revisions]
290
 
    return view_revisions
291
 
 
292
 
 
293
 
def _linear_view_revisions(branch):
294
 
    start_revno, start_revision_id = branch.last_revision_info()
295
 
    repo = branch.repository
296
 
    revision_ids = repo.iter_reverse_revision_history(start_revision_id)
297
 
    for num, revision_id in enumerate(revision_ids):
298
 
        yield revision_id, str(start_revno - num), 0
299
 
 
300
 
 
301
 
def _iter_revisions(repository, view_revisions, generate_delta):
302
 
    num = 9
303
 
    view_revisions = iter(view_revisions)
304
 
    while True:
305
 
        cur_view_revisions = [d for x, d in zip(range(num), view_revisions)]
306
 
        if len(cur_view_revisions) == 0:
307
 
            break
308
 
        cur_deltas = {}
309
 
        # r = revision, n = revno, d = merge depth
310
 
        revision_ids = [r for (r, n, d) in cur_view_revisions]
311
 
        revisions = repository.get_revisions(revision_ids)
312
 
        if generate_delta:
313
 
            deltas = repository.get_deltas_for_revisions(revisions)
314
 
            cur_deltas = dict(izip((r.revision_id for r in revisions),
315
 
                                   deltas))
316
 
        for view_data, revision in izip(cur_view_revisions, revisions):
317
 
            yield view_data, revision, cur_deltas.get(revision.revision_id)
318
 
        num = min(int(num * 1.5), 200)
319
 
 
320
 
 
321
 
def _get_mainline_revs(branch, start_revision, end_revision):
322
 
    """Get the mainline revisions from the branch.
323
 
    
324
 
    Generates the list of mainline revisions for the branch.
325
 
    
326
 
    :param  branch: The branch containing the revisions. 
327
 
 
328
 
    :param  start_revision: The first revision to be logged.
329
 
            For backwards compatibility this may be a mainline integer revno,
330
 
            but for merge revision support a RevisionInfo is expected.
331
 
 
332
 
    :param  end_revision: The last revision to be logged.
333
 
            For backwards compatibility this may be a mainline integer revno,
334
 
            but for merge revision support a RevisionInfo is expected.
335
 
 
336
 
    :return: A (mainline_revs, rev_nos, start_rev_id, end_rev_id) tuple.
337
 
    """
338
192
    which_revs = _enumerate_history(branch)
339
 
    if not which_revs:
340
 
        return None, None, None, None
341
 
 
342
 
    # For mainline generation, map start_revision and end_revision to 
343
 
    # mainline revnos. If the revision is not on the mainline choose the 
344
 
    # appropriate extreme of the mainline instead - the extra will be 
345
 
    # filtered later.
346
 
    # Also map the revisions to rev_ids, to be used in the later filtering
347
 
    # stage.
348
 
    start_rev_id = None 
 
193
    
349
194
    if start_revision is None:
350
 
        start_revno = 1
 
195
        start_revision = 1
351
196
    else:
352
 
        if isinstance(start_revision,RevisionInfo):
353
 
            start_rev_id = start_revision.rev_id
354
 
            start_revno = start_revision.revno or 1
355
 
        else:
356
 
            branch.check_real_revno(start_revision)
357
 
            start_revno = start_revision
 
197
        branch.check_real_revno(start_revision)
358
198
    
359
 
    end_rev_id = None
360
199
    if end_revision is None:
361
 
        end_revno = len(which_revs)
 
200
        end_revision = len(which_revs)
362
201
    else:
363
 
        if isinstance(end_revision,RevisionInfo):
364
 
            end_rev_id = end_revision.rev_id
365
 
            end_revno = end_revision.revno or len(which_revs)
366
 
        else:
367
 
            branch.check_real_revno(end_revision)
368
 
            end_revno = end_revision
369
 
 
370
 
    if ((start_rev_id == NULL_REVISION)
371
 
        or (end_rev_id == NULL_REVISION)):
372
 
        raise BzrCommandError('Logging revision 0 is invalid.')
373
 
    if start_revno > end_revno:
374
 
        raise BzrCommandError("Start revision must be older than "
375
 
                              "the end revision.")
 
202
        branch.check_real_revno(end_revision)
376
203
 
377
204
    # list indexes are 0-based; revisions are 1-based
378
 
    cut_revs = which_revs[(start_revno-1):(end_revno)]
 
205
    cut_revs = which_revs[(start_revision-1):(end_revision)]
379
206
    if not cut_revs:
380
 
        return None, None, None, None
381
 
 
382
 
    # convert the revision history to a dictionary:
383
 
    rev_nos = dict((k, v) for v, k in cut_revs)
384
 
 
 
207
        return
385
208
    # override the mainline to look like the revision history.
386
209
    mainline_revs = [revision_id for index, revision_id in cut_revs]
387
210
    if cut_revs[0][0] == 1:
388
211
        mainline_revs.insert(0, None)
389
212
    else:
390
 
        mainline_revs.insert(0, which_revs[start_revno-2][1])
391
 
    return mainline_revs, rev_nos, start_rev_id, end_rev_id
392
 
 
393
 
 
394
 
def _filter_revision_range(view_revisions, start_rev_id, end_rev_id):
395
 
    """Filter view_revisions based on revision ranges.
396
 
 
397
 
    :param view_revisions: A list of (revision_id, dotted_revno, merge_depth) 
398
 
            tuples to be filtered.
399
 
 
400
 
    :param start_rev_id: If not NONE specifies the first revision to be logged.
401
 
            If NONE then all revisions up to the end_rev_id are logged.
402
 
 
403
 
    :param end_rev_id: If not NONE specifies the last revision to be logged.
404
 
            If NONE then all revisions up to the end of the log are logged.
405
 
 
406
 
    :return: The filtered view_revisions.
407
 
    """
408
 
    if start_rev_id or end_rev_id: 
409
 
        revision_ids = [r for r, n, d in view_revisions]
410
 
        if start_rev_id:
411
 
            start_index = revision_ids.index(start_rev_id)
412
 
        else:
413
 
            start_index = 0
414
 
        if start_rev_id == end_rev_id:
415
 
            end_index = start_index
416
 
        else:
417
 
            if end_rev_id:
418
 
                end_index = revision_ids.index(end_rev_id)
419
 
            else:
420
 
                end_index = len(view_revisions) - 1
421
 
        # To include the revisions merged into the last revision, 
422
 
        # extend end_rev_id down to, but not including, the next rev
423
 
        # with the same or lesser merge_depth
424
 
        end_merge_depth = view_revisions[end_index][2]
425
 
        try:
426
 
            for index in xrange(end_index+1, len(view_revisions)+1):
427
 
                if view_revisions[index][2] <= end_merge_depth:
428
 
                    end_index = index - 1
429
 
                    break
430
 
        except IndexError:
431
 
            # if the search falls off the end then log to the end as well
432
 
            end_index = len(view_revisions) - 1
433
 
        view_revisions = view_revisions[start_index:end_index+1]
434
 
    return view_revisions
435
 
 
436
 
 
437
 
def _filter_revisions_touching_file_id(branch, file_id, mainline_revisions,
438
 
                                       view_revs_iter):
439
 
    """Return the list of revision ids which touch a given file id.
440
 
 
441
 
    The function filters view_revisions and returns a subset.
442
 
    This includes the revisions which directly change the file id,
443
 
    and the revisions which merge these changes. So if the
444
 
    revision graph is::
445
 
        A
446
 
        |\
447
 
        B C
448
 
        |/
449
 
        D
450
 
 
451
 
    And 'C' changes a file, then both C and D will be returned.
452
 
 
453
 
    This will also can be restricted based on a subset of the mainline.
454
 
 
455
 
    :return: A list of (revision_id, dotted_revno, merge_depth) tuples.
456
 
    """
457
 
    # find all the revisions that change the specific file
458
 
    file_weave = branch.repository.weave_store.get_weave(file_id,
459
 
                branch.repository.get_transaction())
460
 
    weave_modifed_revisions = set(file_weave.versions())
461
 
    # build the ancestry of each revision in the graph
462
 
    # - only listing the ancestors that change the specific file.
463
 
    graph = branch.repository.get_graph()
464
 
    # This asks for all mainline revisions, which means we only have to spider
465
 
    # sideways, rather than depth history. That said, its still size-of-history
466
 
    # and should be addressed.
467
 
    # mainline_revisions always includes an extra revision at the beginning, so
468
 
    # don't request it.
469
 
    parent_map = dict(((key, value) for key, value in
470
 
        graph.iter_ancestry(mainline_revisions[1:]) if value is not None))
471
 
    sorted_rev_list = topo_sort(parent_map.items())
472
 
    ancestry = {}
473
 
    for rev in sorted_rev_list:
474
 
        parents = parent_map[rev]
475
 
        if rev not in weave_modifed_revisions and len(parents) == 1:
476
 
            # We will not be adding anything new, so just use a reference to
477
 
            # the parent ancestry.
478
 
            rev_ancestry = ancestry[parents[0]]
479
 
        else:
480
 
            rev_ancestry = set()
481
 
            if rev in weave_modifed_revisions:
482
 
                rev_ancestry.add(rev)
483
 
            for parent in parents:
484
 
                if parent not in ancestry:
485
 
                    # parent is a Ghost, which won't be present in
486
 
                    # sorted_rev_list, but we may access it later, so create an
487
 
                    # empty node for it
488
 
                    ancestry[parent] = set()
489
 
                rev_ancestry = rev_ancestry.union(ancestry[parent])
490
 
        ancestry[rev] = rev_ancestry
491
 
 
492
 
    def is_merging_rev(r):
493
 
        parents = parent_map[r]
494
 
        if len(parents) > 1:
495
 
            leftparent = parents[0]
496
 
            for rightparent in parents[1:]:
497
 
                if not ancestry[leftparent].issuperset(
498
 
                        ancestry[rightparent]):
499
 
                    return True
500
 
        return False
501
 
 
502
 
    # filter from the view the revisions that did not change or merge 
503
 
    # the specific file
504
 
    return [(r, n, d) for r, n, d in view_revs_iter
505
 
            if r in weave_modifed_revisions or is_merging_rev(r)]
506
 
 
507
 
 
508
 
def get_view_revisions(mainline_revs, rev_nos, branch, direction,
509
 
                       include_merges=True):
510
 
    """Produce an iterator of revisions to show
511
 
    :return: an iterator of (revision_id, revno, merge_depth)
512
 
    (if there is no revno for a revision, None is supplied)
513
 
    """
514
 
    if include_merges is False:
515
 
        revision_ids = mainline_revs[1:]
516
 
        if direction == 'reverse':
517
 
            revision_ids.reverse()
518
 
        for revision_id in revision_ids:
519
 
            yield revision_id, str(rev_nos[revision_id]), 0
520
 
        return
521
 
    graph = branch.repository.get_graph()
522
 
    # This asks for all mainline revisions, which means we only have to spider
523
 
    # sideways, rather than depth history. That said, its still size-of-history
524
 
    # and should be addressed.
525
 
    # mainline_revisions always includes an extra revision at the beginning, so
526
 
    # don't request it.
527
 
    parent_map = dict(((key, value) for key, value in
528
 
        graph.iter_ancestry(mainline_revs[1:]) if value is not None))
529
 
    # filter out ghosts; merge_sort errors on ghosts.
530
 
    rev_graph = _strip_NULL_ghosts(parent_map)
 
213
        mainline_revs.insert(0, which_revs[start_revision-2][1])
 
214
 
531
215
    merge_sorted_revisions = merge_sort(
532
 
        rev_graph,
 
216
        branch.repository.get_revision_graph(mainline_revs[-1]),
533
217
        mainline_revs[-1],
534
 
        mainline_revs,
535
 
        generate_revno=True)
 
218
        mainline_revs)
536
219
 
537
 
    if direction == 'forward':
 
220
    if direction == 'reverse':
 
221
        cut_revs.reverse()
 
222
    elif direction == 'forward':
538
223
        # forward means oldest first.
539
 
        merge_sorted_revisions = reverse_by_depth(merge_sorted_revisions)
540
 
    elif direction != 'reverse':
 
224
        merge_sorted_revisions.reverse()
 
225
    else:
541
226
        raise ValueError('invalid direction %r' % direction)
542
227
 
543
 
    for sequence, rev_id, merge_depth, revno, end_of_merge in merge_sorted_revisions:
544
 
        yield rev_id, '.'.join(map(str, revno)), merge_depth
545
 
 
546
 
 
547
 
def reverse_by_depth(merge_sorted_revisions, _depth=0):
548
 
    """Reverse revisions by depth.
549
 
 
550
 
    Revisions with a different depth are sorted as a group with the previous
551
 
    revision of that depth.  There may be no topological justification for this,
552
 
    but it looks much nicer.
553
 
    """
554
 
    zd_revisions = []
555
 
    for val in merge_sorted_revisions:
556
 
        if val[2] == _depth:
557
 
            zd_revisions.append([val])
 
228
    revision_history = branch.revision_history()
 
229
 
 
230
    # convert the revision history to a dictionary:
 
231
    rev_nos = {}
 
232
    for index, rev_id in cut_revs:
 
233
        rev_nos[rev_id] = index
 
234
 
 
235
    # now we just print all the revisions
 
236
    for sequence, rev_id, merge_depth, end_of_merge in merge_sorted_revisions:
 
237
        rev = branch.repository.get_revision(rev_id)
 
238
 
 
239
        if searchRE:
 
240
            if not searchRE.search(rev.message):
 
241
                continue
 
242
 
 
243
        if merge_depth == 0:
 
244
            # a mainline revision.
 
245
            if verbose or specific_fileid:
 
246
                delta = _get_revision_delta(branch, rev_nos[rev_id])
 
247
                
 
248
            if specific_fileid:
 
249
                if not delta.touches_file_id(specific_fileid):
 
250
                    continue
 
251
    
 
252
            if not verbose:
 
253
                # although we calculated it, throw it away without display
 
254
                delta = None
 
255
 
 
256
            lf.show(rev_nos[rev_id], rev, delta)
 
257
        elif hasattr(lf, 'show_merge'):
 
258
            lf.show_merge(rev, merge_depth)
 
259
 
 
260
 
 
261
def deltas_for_log_dummy(branch, which_revs):
 
262
    """Return all the revisions without intermediate deltas.
 
263
 
 
264
    Useful for log commands that won't need the delta information.
 
265
    """
 
266
    
 
267
    for revno, revision_id in which_revs:
 
268
        yield revno, branch.get_revision(revision_id), None
 
269
 
 
270
 
 
271
def deltas_for_log_reverse(branch, which_revs):
 
272
    """Compute deltas for display in latest-to-earliest order.
 
273
 
 
274
    branch
 
275
        Branch to traverse
 
276
 
 
277
    which_revs
 
278
        Sequence of (revno, revision_id) for the subset of history to examine
 
279
 
 
280
    returns 
 
281
        Sequence of (revno, rev, delta)
 
282
 
 
283
    The delta is from the given revision to the next one in the
 
284
    sequence, which makes sense if the log is being displayed from
 
285
    newest to oldest.
 
286
    """
 
287
    last_revno = last_revision_id = last_tree = None
 
288
    for revno, revision_id in which_revs:
 
289
        this_tree = branch.revision_tree(revision_id)
 
290
        this_revision = branch.get_revision(revision_id)
 
291
        
 
292
        if last_revno:
 
293
            yield last_revno, last_revision, compare_trees(this_tree, last_tree, False)
 
294
 
 
295
        this_tree = EmptyTree(branch.get_root_id())
 
296
 
 
297
        last_revno = revno
 
298
        last_revision = this_revision
 
299
        last_tree = this_tree
 
300
 
 
301
    if last_revno:
 
302
        if last_revno == 1:
 
303
            this_tree = EmptyTree(branch.get_root_id())
558
304
        else:
559
 
            zd_revisions[-1].append(val)
560
 
    for revisions in zd_revisions:
561
 
        if len(revisions) > 1:
562
 
            revisions[1:] = reverse_by_depth(revisions[1:], _depth + 1)
563
 
    zd_revisions.reverse()
564
 
    result = []
565
 
    for chunk in zd_revisions:
566
 
        result.extend(chunk)
567
 
    return result
568
 
 
569
 
 
570
 
class LogRevision(object):
571
 
    """A revision to be logged (by LogFormatter.log_revision).
572
 
 
573
 
    A simple wrapper for the attributes of a revision to be logged.
574
 
    The attributes may or may not be populated, as determined by the 
575
 
    logging options and the log formatter capabilities.
 
305
            this_revno = last_revno - 1
 
306
            this_revision_id = branch.revision_history()[this_revno]
 
307
            this_tree = branch.revision_tree(this_revision_id)
 
308
        yield last_revno, last_revision, compare_trees(this_tree, last_tree, False)
 
309
 
 
310
 
 
311
def deltas_for_log_forward(branch, which_revs):
 
312
    """Compute deltas for display in forward log.
 
313
 
 
314
    Given a sequence of (revno, revision_id) pairs, return
 
315
    (revno, rev, delta).
 
316
 
 
317
    The delta is from the given revision to the next one in the
 
318
    sequence, which makes sense if the log is being displayed from
 
319
    newest to oldest.
576
320
    """
577
 
 
578
 
    def __init__(self, rev=None, revno=None, merge_depth=0, delta=None,
579
 
                 tags=None):
580
 
        self.rev = rev
581
 
        self.revno = revno
582
 
        self.merge_depth = merge_depth
583
 
        self.delta = delta
584
 
        self.tags = tags
 
321
    last_revno = last_revision_id = last_tree = None
 
322
    prev_tree = EmptyTree(branch.get_root_id())
 
323
 
 
324
    for revno, revision_id in which_revs:
 
325
        this_tree = branch.revision_tree(revision_id)
 
326
        this_revision = branch.get_revision(revision_id)
 
327
 
 
328
        if not last_revno:
 
329
            if revno == 1:
 
330
                last_tree = EmptyTree(branch.get_root_id())
 
331
            else:
 
332
                last_revno = revno - 1
 
333
                last_revision_id = branch.revision_history()[last_revno]
 
334
                last_tree = branch.revision_tree(last_revision_id)
 
335
 
 
336
        yield revno, this_revision, compare_trees(last_tree, this_tree, False)
 
337
 
 
338
        last_revno = revno
 
339
        last_revision = this_revision
 
340
        last_tree = this_tree
585
341
 
586
342
 
587
343
class LogFormatter(object):
588
 
    """Abstract class to display log messages.
589
 
 
590
 
    At a minimum, a derived class must implement the log_revision method.
591
 
 
592
 
    If the LogFormatter needs to be informed of the beginning or end of
593
 
    a log it should implement the begin_log and/or end_log hook methods.
594
 
 
595
 
    A LogFormatter should define the following supports_XXX flags 
596
 
    to indicate which LogRevision attributes it supports:
597
 
 
598
 
    - supports_delta must be True if this log formatter supports delta.
599
 
        Otherwise the delta attribute may not be populated.
600
 
    - supports_merge_revisions must be True if this log formatter supports 
601
 
        merge revisions.  If not, and if supports_single_merge_revisions is
602
 
        also not True, then only mainline revisions will be passed to the 
603
 
        formatter.
604
 
    - supports_single_merge_revision must be True if this log formatter
605
 
        supports logging only a single merge revision.  This flag is
606
 
        only relevant if supports_merge_revisions is not True.
607
 
    - supports_tags must be True if this log formatter supports tags.
608
 
        Otherwise the tags attribute may not be populated.
609
 
    """
610
 
 
 
344
    """Abstract class to display log messages."""
611
345
    def __init__(self, to_file, show_ids=False, show_timezone='original'):
612
346
        self.to_file = to_file
613
347
        self.show_ids = show_ids
614
348
        self.show_timezone = show_timezone
615
349
 
616
 
# TODO: uncomment this block after show() has been removed.
617
 
# Until then defining log_revision would prevent _show_log calling show() 
618
 
# in legacy formatters.
619
 
#    def log_revision(self, revision):
620
 
#        """Log a revision.
621
 
#
622
 
#        :param  revision:   The LogRevision to be logged.
623
 
#        """
624
 
#        raise NotImplementedError('not implemented in abstract base')
 
350
 
 
351
    def show(self, revno, rev, delta):
 
352
        raise NotImplementedError('not implemented in abstract base')
625
353
 
626
354
    def short_committer(self, rev):
627
 
        name, address = config.parse_username(rev.committer)
628
 
        if name:
629
 
            return name
630
 
        return address
631
 
 
632
 
    def short_author(self, rev):
633
 
        name, address = config.parse_username(rev.get_apparent_author())
634
 
        if name:
635
 
            return name
636
 
        return address
637
 
 
638
 
 
 
355
        return re.sub('<.*@.*>', '', rev.committer).strip(' ')
 
356
    
 
357
    
639
358
class LongLogFormatter(LogFormatter):
640
 
 
641
 
    supports_merge_revisions = True
642
 
    supports_delta = True
643
 
    supports_tags = True
644
 
 
645
 
    def log_revision(self, revision):
646
 
        """Log a revision, either merged or not."""
647
 
        indent = '    ' * revision.merge_depth
 
359
    def show(self, revno, rev, delta):
 
360
        return self._show_helper(revno=revno, rev=rev, delta=delta)
 
361
 
 
362
    def show_merge(self, rev, merge_depth):
 
363
        return self._show_helper(rev=rev, indent='    '*merge_depth, merged=True, delta=None)
 
364
 
 
365
    def _show_helper(self, rev=None, revno=None, indent='', merged=False, delta=None):
 
366
        """Show a revision, either merged or not."""
 
367
        from bzrlib.osutils import format_date
648
368
        to_file = self.to_file
649
 
        to_file.write(indent + '-' * 60 + '\n')
650
 
        if revision.revno is not None:
651
 
            to_file.write(indent + 'revno: %s\n' % (revision.revno,))
652
 
        if revision.tags:
653
 
            to_file.write(indent + 'tags: %s\n' % (', '.join(revision.tags)))
 
369
        print >>to_file,  indent+'-' * 60
 
370
        if revno is not None:
 
371
            print >>to_file,  'revno:', revno
 
372
        if merged:
 
373
            print >>to_file,  indent+'merged:', rev.revision_id
 
374
        elif self.show_ids:
 
375
            print >>to_file,  indent+'revision-id:', rev.revision_id
654
376
        if self.show_ids:
655
 
            to_file.write(indent + 'revision-id: ' + revision.rev.revision_id)
656
 
            to_file.write('\n')
657
 
            for parent_id in revision.rev.parent_ids:
658
 
                to_file.write(indent + 'parent: %s\n' % (parent_id,))
659
 
 
660
 
        author = revision.rev.properties.get('author', None)
661
 
        if author is not None:
662
 
            to_file.write(indent + 'author: %s\n' % (author,))
663
 
        to_file.write(indent + 'committer: %s\n' % (revision.rev.committer,))
664
 
 
665
 
        branch_nick = revision.rev.properties.get('branch-nick', None)
666
 
        if branch_nick is not None:
667
 
            to_file.write(indent + 'branch nick: %s\n' % (branch_nick,))
668
 
 
669
 
        date_str = format_date(revision.rev.timestamp,
670
 
                               revision.rev.timezone or 0,
 
377
            for parent_id in rev.parent_ids:
 
378
                print >>to_file, indent+'parent:', parent_id
 
379
        print >>to_file,  indent+'committer:', rev.committer
 
380
        try:
 
381
            print >>to_file, indent+'branch nick: %s' % \
 
382
                rev.properties['branch-nick']
 
383
        except KeyError:
 
384
            pass
 
385
        date_str = format_date(rev.timestamp,
 
386
                               rev.timezone or 0,
671
387
                               self.show_timezone)
672
 
        to_file.write(indent + 'timestamp: %s\n' % (date_str,))
 
388
        print >>to_file,  indent+'timestamp: %s' % date_str
673
389
 
674
 
        to_file.write(indent + 'message:\n')
675
 
        if not revision.rev.message:
676
 
            to_file.write(indent + '  (no message)\n')
 
390
        print >>to_file,  indent+'message:'
 
391
        if not rev.message:
 
392
            print >>to_file,  indent+'  (no message)'
677
393
        else:
678
 
            message = revision.rev.message.rstrip('\r\n')
 
394
            message = rev.message.rstrip('\r\n')
679
395
            for l in message.split('\n'):
680
 
                to_file.write(indent + '  %s\n' % (l,))
681
 
        if revision.delta is not None:
682
 
            revision.delta.show(to_file, self.show_ids, indent=indent)
 
396
                print >>to_file,  indent+'  ' + l
 
397
        if delta != None:
 
398
            delta.show(to_file, self.show_ids)
683
399
 
684
400
 
685
401
class ShortLogFormatter(LogFormatter):
686
 
 
687
 
    supports_delta = True
688
 
    supports_single_merge_revision = True
689
 
 
690
 
    def log_revision(self, revision):
 
402
    def show(self, revno, rev, delta):
 
403
        from bzrlib.osutils import format_date
 
404
 
691
405
        to_file = self.to_file
692
 
        date_str = format_date(revision.rev.timestamp,
693
 
                               revision.rev.timezone or 0,
694
 
                               self.show_timezone)
695
 
        is_merge = ''
696
 
        if len(revision.rev.parent_ids) > 1:
697
 
            is_merge = ' [merge]'
698
 
        to_file.write("%5s %s\t%s%s\n" % (revision.revno,
699
 
                self.short_author(revision.rev),
700
 
                format_date(revision.rev.timestamp,
701
 
                            revision.rev.timezone or 0,
 
406
        date_str = format_date(rev.timestamp, rev.timezone or 0,
 
407
                            self.show_timezone)
 
408
        print >>to_file, "%5d %s\t%s" % (revno, self.short_committer(rev),
 
409
                format_date(rev.timestamp, rev.timezone or 0,
702
410
                            self.show_timezone, date_fmt="%Y-%m-%d",
703
 
                            show_offset=False),
704
 
                is_merge))
 
411
                           show_offset=False))
705
412
        if self.show_ids:
706
 
            to_file.write('      revision-id:%s\n' % (revision.rev.revision_id,))
707
 
        if not revision.rev.message:
708
 
            to_file.write('      (no message)\n')
 
413
            print >>to_file,  '      revision-id:', rev.revision_id
 
414
        if not rev.message:
 
415
            print >>to_file,  '      (no message)'
709
416
        else:
710
 
            message = revision.rev.message.rstrip('\r\n')
 
417
            message = rev.message.rstrip('\r\n')
711
418
            for l in message.split('\n'):
712
 
                to_file.write('      %s\n' % (l,))
 
419
                print >>to_file,  '      ' + l
713
420
 
714
421
        # TODO: Why not show the modified files in a shorter form as
715
422
        # well? rewrap them single lines of appropriate length
716
 
        if revision.delta is not None:
717
 
            revision.delta.show(to_file, self.show_ids)
718
 
        to_file.write('\n')
719
 
 
 
423
        if delta != None:
 
424
            delta.show(to_file, self.show_ids)
 
425
        print >>to_file, ''
720
426
 
721
427
class LineLogFormatter(LogFormatter):
722
 
 
723
 
    supports_single_merge_revision = True
724
 
 
725
 
    def __init__(self, *args, **kwargs):
726
 
        super(LineLogFormatter, self).__init__(*args, **kwargs)
727
 
        self._max_chars = terminal_width() - 1
728
 
 
729
428
    def truncate(self, str, max_len):
730
429
        if len(str) <= max_len:
731
430
            return str
732
431
        return str[:max_len-3]+'...'
733
432
 
734
433
    def date_string(self, rev):
 
434
        from bzrlib.osutils import format_date
735
435
        return format_date(rev.timestamp, rev.timezone or 0, 
736
436
                           self.show_timezone, date_fmt="%Y-%m-%d",
737
437
                           show_offset=False)
742
442
        else:
743
443
            return rev.message
744
444
 
745
 
    def log_revision(self, revision):
746
 
        self.to_file.write(self.log_string(revision.revno, revision.rev,
747
 
                                              self._max_chars))
748
 
        self.to_file.write('\n')
 
445
    def show(self, revno, rev, delta):
 
446
        print >> self.to_file, self.log_string(rev, 79) 
749
447
 
750
 
    def log_string(self, revno, rev, max_chars):
751
 
        """Format log info into one string. Truncate tail of string
752
 
        :param  revno:      revision number (int) or None.
753
 
                            Revision numbers counts from 1.
754
 
        :param  rev:        revision info object
755
 
        :param  max_chars:  maximum length of resulting string
756
 
        :return:            formatted truncated string
757
 
        """
758
 
        out = []
759
 
        if revno:
760
 
            # show revno only when is not None
761
 
            out.append("%s:" % revno)
762
 
        out.append(self.truncate(self.short_author(rev), 20))
 
448
    def log_string(self, rev, max_chars):
 
449
        out = [self.truncate(self.short_committer(rev), 20)]
763
450
        out.append(self.date_string(rev))
764
 
        out.append(rev.get_summary())
 
451
        out.append(self.message(rev).replace('\n', ' '))
765
452
        return self.truncate(" ".join(out).rstrip('\n'), max_chars)
766
453
 
767
 
 
768
454
def line_log(rev, max_chars):
769
455
    lf = LineLogFormatter(None)
770
 
    return lf.log_string(None, rev, max_chars)
771
 
 
772
 
 
773
 
class LogFormatterRegistry(registry.Registry):
774
 
    """Registry for log formatters"""
775
 
 
776
 
    def make_formatter(self, name, *args, **kwargs):
777
 
        """Construct a formatter from arguments.
778
 
 
779
 
        :param name: Name of the formatter to construct.  'short', 'long' and
780
 
            'line' are built-in.
781
 
        """
782
 
        return self.get(name)(*args, **kwargs)
783
 
 
784
 
    def get_default(self, branch):
785
 
        return self.get(branch.get_config().log_format())
786
 
 
787
 
 
788
 
log_formatter_registry = LogFormatterRegistry()
789
 
 
790
 
 
791
 
log_formatter_registry.register('short', ShortLogFormatter,
792
 
                                'Moderately short log format')
793
 
log_formatter_registry.register('long', LongLogFormatter,
794
 
                                'Detailed log format')
795
 
log_formatter_registry.register('line', LineLogFormatter,
796
 
                                'Log format with one line per revision')
797
 
 
 
456
    return lf.log_string(rev, max_chars)
 
457
 
 
458
FORMATTERS = {
 
459
              'long': LongLogFormatter,
 
460
              'short': ShortLogFormatter,
 
461
              'line': LineLogFormatter,
 
462
              }
798
463
 
799
464
def register_formatter(name, formatter):
800
 
    log_formatter_registry.register(name, formatter)
801
 
 
 
465
    FORMATTERS[name] = formatter
802
466
 
803
467
def log_formatter(name, *args, **kwargs):
804
468
    """Construct a formatter from arguments.
806
470
    name -- Name of the formatter to construct; currently 'long', 'short' and
807
471
        'line' are supported.
808
472
    """
 
473
    from bzrlib.errors import BzrCommandError
809
474
    try:
810
 
        return log_formatter_registry.make_formatter(name, *args, **kwargs)
 
475
        return FORMATTERS[name](*args, **kwargs)
811
476
    except KeyError:
812
477
        raise BzrCommandError("unknown log formatter: %r" % name)
813
478
 
814
 
 
815
479
def show_one_log(revno, rev, delta, verbose, to_file, show_timezone):
816
 
    # deprecated; for compatibility
 
480
    # deprecated; for compatability
817
481
    lf = LongLogFormatter(to_file=to_file, show_timezone=show_timezone)
818
482
    lf.show(revno, rev, delta)
819
483
 
820
 
 
821
 
def show_changed_revisions(branch, old_rh, new_rh, to_file=None,
822
 
                           log_format='long'):
 
484
def show_changed_revisions(branch, old_rh, new_rh, to_file=None, log_format='long'):
823
485
    """Show the change in revision history comparing the old revision history to the new one.
824
486
 
825
487
    :param branch: The branch where the revisions exist
828
490
    :param to_file: A file to write the results to. If None, stdout will be used
829
491
    """
830
492
    if to_file is None:
831
 
        to_file = codecs.getwriter(get_terminal_encoding())(sys.stdout,
832
 
            errors='replace')
 
493
        import sys
 
494
        import codecs
 
495
        import bzrlib
 
496
        to_file = codecs.getwriter(bzrlib.user_encoding)(sys.stdout, errors='replace')
833
497
    lf = log_formatter(log_format,
834
498
                       show_ids=False,
835
499
                       to_file=to_file,
857
521
        to_file.write('\nRemoved Revisions:\n')
858
522
        for i in range(base_idx, len(old_rh)):
859
523
            rev = branch.repository.get_revision(old_rh[i])
860
 
            lr = LogRevision(rev, i+1, 0, None)
861
 
            lf.log_revision(lr)
 
524
            lf.show(i+1, rev, None)
862
525
        to_file.write('*'*60)
863
526
        to_file.write('\n\n')
864
527
    if base_idx < len(new_rh):
866
529
        show_log(branch,
867
530
                 lf,
868
531
                 None,
869
 
                 verbose=False,
 
532
                 verbose=True,
870
533
                 direction='forward',
871
534
                 start_revision=base_idx+1,
872
535
                 end_revision=len(new_rh),