/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: Lukáš Lalinský
  • Date: 2007-12-02 18:59:28 UTC
  • mto: This revision was merged to the branch mainline in revision 3080.
  • Revision ID: lalinsky@gmail.com-20071202185928-00cgoqzowl3s87hi
Move the name and e-mail address extraction logic to config.parse_username.

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