/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: Martin Pool
  • Date: 2007-08-15 04:33:34 UTC
  • mto: (2701.1.2 remove-should-cache)
  • mto: This revision was merged to the branch mainline in revision 2710.
  • Revision ID: mbp@sourcefrog.net-20070815043334-01dx9emb0vjiy29v
Remove things deprecated in 0.11 and earlier

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