~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/log.py

  • Committer: Erik Bågfors
  • Date: 2006-02-03 19:50:59 UTC
  • mto: (1185.50.77 bzr-jam-integration)
  • mto: This revision was merged to the branch mainline in revision 1554.
  • Revision ID: erik@bagfors.nu-20060203195059-1cf8ff5aa68de0ea
Support for plugins to register log formatters and set default formatter
Also, change one command line option for "log"

Show diffs side-by-side

added added

removed removed

Lines of Context:
28
28
 
29
29
* with file-ids and revision-ids shown
30
30
 
31
 
* from last to first or (not anymore) from first to last;
32
 
  the default is "reversed" because it shows the likely most
33
 
  relevant and interesting information first
34
 
 
35
 
* (not yet) in XML format
 
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 mainline 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.
36
50
"""
37
51
 
38
52
 
39
 
from trace import mutter
 
53
# TODO: option to show delta summaries for merged-in revisions
 
54
 
 
55
import bzrlib.errors as errors
 
56
from bzrlib.tree import EmptyTree
 
57
from bzrlib.delta import compare_trees
 
58
from bzrlib.trace import mutter
 
59
import re
 
60
 
40
61
 
41
62
def find_touching_revisions(branch, file_id):
42
63
    """Yield a description of revisions which affect the file_id.
53
74
    last_path = None
54
75
    revno = 1
55
76
    for revision_id in branch.revision_history():
56
 
        this_inv = branch.get_revision_inventory(revision_id)
 
77
        this_inv = branch.repository.get_revision_inventory(revision_id)
57
78
        if file_id in this_inv:
58
79
            this_ie = this_inv[file_id]
59
80
            this_path = this_inv.id2path(file_id)
83
104
 
84
105
 
85
106
 
 
107
def _enumerate_history(branch):
 
108
    rh = []
 
109
    revno = 1
 
110
    for rev_id in branch.revision_history():
 
111
        rh.append((revno, rev_id))
 
112
        revno += 1
 
113
    return rh
 
114
 
 
115
 
 
116
def _get_revision_delta(branch, revno):
 
117
    """Return the delta for a mainline revision.
 
118
    
 
119
    This is used to show summaries in verbose logs, and also for finding 
 
120
    revisions which touch a given file."""
 
121
    # XXX: What are we supposed to do when showing a summary for something 
 
122
    # other than a mainline revision.  The delta to it's first parent, or
 
123
    # (more useful) the delta to a nominated other revision.
 
124
    return branch.get_revision_delta(revno)
 
125
 
 
126
 
86
127
def show_log(branch,
87
128
             lf,
88
129
             specific_fileid=None,
113
154
    end_revision
114
155
        If not None, only show revisions <= end_revision
115
156
    """
 
157
    branch.lock_read()
 
158
    try:
 
159
        _show_log(branch, lf, specific_fileid, verbose, direction,
 
160
                  start_revision, end_revision, search)
 
161
    finally:
 
162
        branch.unlock()
 
163
    
 
164
def _show_log(branch,
 
165
             lf,
 
166
             specific_fileid=None,
 
167
             verbose=False,
 
168
             direction='reverse',
 
169
             start_revision=None,
 
170
             end_revision=None,
 
171
             search=None):
 
172
    """Worker function for show_log - see show_log."""
116
173
    from bzrlib.osutils import format_date
117
174
    from bzrlib.errors import BzrCheckError
118
175
    from bzrlib.textui import show_status
123
180
        warn("not a LogFormatter instance: %r" % lf)
124
181
 
125
182
    if specific_fileid:
126
 
        mutter('get log for file_id %r' % specific_fileid)
 
183
        mutter('get log for file_id %r', specific_fileid)
127
184
 
128
185
    if search is not None:
129
186
        import re
131
188
    else:
132
189
        searchRE = None
133
190
 
134
 
    which_revs = branch.enum_history(direction)
135
 
    which_revs = [x for x in which_revs if (
136
 
            (start_revision is None or x[0] >= start_revision)
137
 
            and (end_revision is None or x[0] <= end_revision))]
138
 
 
139
 
    if not (verbose or specific_fileid):
140
 
        # no need to know what changed between revisions
141
 
        with_deltas = deltas_for_log_dummy(branch, which_revs)
142
 
    elif direction == 'reverse':
143
 
        with_deltas = deltas_for_log_reverse(branch, which_revs)
144
 
    else:        
145
 
        with_deltas = deltas_for_log_forward(branch, which_revs)
146
 
 
147
 
    for revno, rev, delta in with_deltas:
 
191
    which_revs = _enumerate_history(branch)
 
192
    
 
193
    if start_revision is None:
 
194
        start_revision = 1
 
195
    else:
 
196
        branch.check_real_revno(start_revision)
 
197
    
 
198
    if end_revision is None:
 
199
        end_revision = len(which_revs)
 
200
    else:
 
201
        branch.check_real_revno(end_revision)
 
202
 
 
203
    # list indexes are 0-based; revisions are 1-based
 
204
    cut_revs = which_revs[(start_revision-1):(end_revision)]
 
205
 
 
206
    if direction == 'reverse':
 
207
        cut_revs.reverse()
 
208
    elif direction == 'forward':
 
209
        pass
 
210
    else:
 
211
        raise ValueError('invalid direction %r' % direction)
 
212
 
 
213
    revision_history = branch.revision_history()
 
214
    for revno, rev_id in cut_revs:
 
215
        if verbose or specific_fileid:
 
216
            delta = _get_revision_delta(branch, revno)
 
217
            
148
218
        if specific_fileid:
149
219
            if not delta.touches_file_id(specific_fileid):
150
220
                continue
153
223
            # although we calculated it, throw it away without display
154
224
            delta = None
155
225
 
156
 
        if searchRE is None or searchRE.search(rev.message):
157
 
            lf.show(revno, rev, delta)
158
 
 
 
226
        rev = branch.repository.get_revision(rev_id)
 
227
 
 
228
        if searchRE:
 
229
            if not searchRE.search(rev.message):
 
230
                continue
 
231
 
 
232
        lf.show(revno, rev, delta)
 
233
        if hasattr(lf, 'show_merge'):
 
234
            if revno == 1:
 
235
                excludes = set()
 
236
            else:
 
237
                # revno is 1 based, so -2 to get back 1 less.
 
238
                repository = branch.repository
 
239
                excludes = repository.get_ancestry(revision_history[revno - 2])
 
240
                excludes = set(excludes)
 
241
            pending = list(rev.parent_ids)
 
242
            while pending:
 
243
                rev_id = pending.pop()
 
244
                if rev_id in excludes:
 
245
                    continue
 
246
                # prevent showing merged revs twice if they multi-path.
 
247
                excludes.add(rev_id)
 
248
                try:
 
249
                    rev = branch.repository.get_revision(rev_id)
 
250
                except errors.NoSuchRevision:
 
251
                    continue
 
252
                pending.extend(rev.parent_ids)
 
253
                lf.show_merge(rev)
159
254
 
160
255
 
161
256
def deltas_for_log_dummy(branch, which_revs):
 
257
    """Return all the revisions without intermediate deltas.
 
258
 
 
259
    Useful for log commands that won't need the delta information.
 
260
    """
 
261
    
162
262
    for revno, revision_id in which_revs:
163
263
        yield revno, branch.get_revision(revision_id), None
164
264
 
165
265
 
166
266
def deltas_for_log_reverse(branch, which_revs):
167
 
    """Compute deltas for display in reverse log.
168
 
 
169
 
    Given a sequence of (revno, revision_id) pairs, return
170
 
    (revno, rev, delta).
 
267
    """Compute deltas for display in latest-to-earliest order.
 
268
 
 
269
    branch
 
270
        Branch to traverse
 
271
 
 
272
    which_revs
 
273
        Sequence of (revno, revision_id) for the subset of history to examine
 
274
 
 
275
    returns 
 
276
        Sequence of (revno, rev, delta)
171
277
 
172
278
    The delta is from the given revision to the next one in the
173
279
    sequence, which makes sense if the log is being displayed from
174
280
    newest to oldest.
175
281
    """
176
 
    from tree import EmptyTree
177
 
    from diff import compare_trees
178
 
    
179
282
    last_revno = last_revision_id = last_tree = None
180
283
    for revno, revision_id in which_revs:
181
284
        this_tree = branch.revision_tree(revision_id)
210
313
    sequence, which makes sense if the log is being displayed from
211
314
    newest to oldest.
212
315
    """
213
 
    from tree import EmptyTree
214
 
    from diff import compare_trees
215
 
 
216
316
    last_revno = last_revision_id = last_tree = None
217
317
    prev_tree = EmptyTree(branch.get_root_id())
218
318
 
237
337
 
238
338
class LogFormatter(object):
239
339
    """Abstract class to display log messages."""
240
 
    def __init__(self, to_file, show_ids=False, show_timezone=False):
 
340
    def __init__(self, to_file, show_ids=False, show_timezone='original'):
241
341
        self.to_file = to_file
242
342
        self.show_ids = show_ids
243
343
        self.show_timezone = show_timezone
244
 
        
245
 
 
246
 
 
247
 
 
248
 
 
249
 
 
 
344
 
 
345
 
 
346
    def show(self, revno, rev, delta):
 
347
        raise NotImplementedError('not implemented in abstract base')
 
348
 
 
349
    def short_committer(self, rev):
 
350
        return re.sub('<.*@.*>', '', rev.committer).strip(' ')
 
351
    
 
352
    
250
353
class LongLogFormatter(LogFormatter):
251
354
    def show(self, revno, rev, delta):
252
 
        from osutils import format_date
253
 
 
 
355
        return self._show_helper(revno=revno, rev=rev, delta=delta)
 
356
 
 
357
    def show_merge(self, rev):
 
358
        return self._show_helper(rev=rev, indent='    ', merged=True, delta=None)
 
359
 
 
360
    def _show_helper(self, rev=None, revno=None, indent='', merged=False, delta=None):
 
361
        """Show a revision, either merged or not."""
 
362
        from bzrlib.osutils import format_date
254
363
        to_file = self.to_file
255
 
 
256
 
        print >>to_file,  '-' * 60
257
 
        print >>to_file,  'revno:', revno
 
364
        print >>to_file,  indent+'-' * 60
 
365
        if revno is not None:
 
366
            print >>to_file,  'revno:', revno
 
367
        if merged:
 
368
            print >>to_file,  indent+'merged:', rev.revision_id
 
369
        elif self.show_ids:
 
370
            print >>to_file,  indent+'revision-id:', rev.revision_id
258
371
        if self.show_ids:
259
 
            print >>to_file,  'revision-id:', rev.revision_id
260
 
        print >>to_file,  'committer:', rev.committer
261
 
        print >>to_file,  'timestamp: %s' % (format_date(rev.timestamp, rev.timezone or 0,
262
 
                                             self.show_timezone))
 
372
            for parent_id in rev.parent_ids:
 
373
                print >>to_file, indent+'parent:', parent_id
 
374
        print >>to_file,  indent+'committer:', rev.committer
 
375
        try:
 
376
            print >>to_file, indent+'branch nick: %s' % \
 
377
                rev.properties['branch-nick']
 
378
        except KeyError:
 
379
            pass
 
380
        date_str = format_date(rev.timestamp,
 
381
                               rev.timezone or 0,
 
382
                               self.show_timezone)
 
383
        print >>to_file,  indent+'timestamp: %s' % date_str
263
384
 
264
 
        print >>to_file,  'message:'
 
385
        print >>to_file,  indent+'message:'
265
386
        if not rev.message:
266
 
            print >>to_file,  '  (no message)'
 
387
            print >>to_file,  indent+'  (no message)'
267
388
        else:
268
 
            for l in rev.message.split('\n'):
269
 
                print >>to_file,  '  ' + l
270
 
 
 
389
            message = rev.message.rstrip('\r\n')
 
390
            for l in message.split('\n'):
 
391
                print >>to_file,  indent+'  ' + l
271
392
        if delta != None:
272
393
            delta.show(to_file, self.show_ids)
273
394
 
274
395
 
275
 
 
276
396
class ShortLogFormatter(LogFormatter):
277
397
    def show(self, revno, rev, delta):
278
398
        from bzrlib.osutils import format_date
279
399
 
280
400
        to_file = self.to_file
281
 
 
282
 
        print >>to_file, "%5d %s\t%s" % (revno, rev.committer,
 
401
        date_str = format_date(rev.timestamp, rev.timezone or 0,
 
402
                            self.show_timezone)
 
403
        print >>to_file, "%5d %s\t%s" % (revno, self.short_committer(rev),
283
404
                format_date(rev.timestamp, rev.timezone or 0,
284
 
                            self.show_timezone))
 
405
                            self.show_timezone, date_fmt="%Y-%m-%d",
 
406
                           show_offset=False))
285
407
        if self.show_ids:
286
408
            print >>to_file,  '      revision-id:', rev.revision_id
287
409
        if not rev.message:
288
410
            print >>to_file,  '      (no message)'
289
411
        else:
290
 
            for l in rev.message.split('\n'):
 
412
            message = rev.message.rstrip('\r\n')
 
413
            for l in message.split('\n'):
291
414
                print >>to_file,  '      ' + l
292
415
 
 
416
        # TODO: Why not show the modified files in a shorter form as
 
417
        # well? rewrap them single lines of appropriate length
293
418
        if delta != None:
294
419
            delta.show(to_file, self.show_ids)
295
 
        print
296
 
 
297
 
 
298
 
 
299
 
FORMATTERS = {'long': LongLogFormatter,
 
420
        print >>to_file, ''
 
421
 
 
422
class LineLogFormatter(LogFormatter):
 
423
    def truncate(self, str, max_len):
 
424
        if len(str) <= max_len:
 
425
            return str
 
426
        return str[:max_len-3]+'...'
 
427
 
 
428
    def date_string(self, rev):
 
429
        from bzrlib.osutils import format_date
 
430
        return format_date(rev.timestamp, rev.timezone or 0, 
 
431
                           self.show_timezone, date_fmt="%Y-%m-%d",
 
432
                           show_offset=False)
 
433
 
 
434
    def message(self, rev):
 
435
        if not rev.message:
 
436
            return '(no message)'
 
437
        else:
 
438
            return rev.message
 
439
 
 
440
    def show(self, revno, rev, delta):
 
441
        print >> self.to_file, self.log_string(rev, 79) 
 
442
 
 
443
    def log_string(self, rev, max_chars):
 
444
        out = [self.truncate(self.short_committer(rev), 20)]
 
445
        out.append(self.date_string(rev))
 
446
        out.append(self.message(rev).replace('\n', ' '))
 
447
        return self.truncate(" ".join(out).rstrip('\n'), max_chars)
 
448
 
 
449
def line_log(rev, max_chars):
 
450
    lf = LineLogFormatter(None)
 
451
    return lf.log_string(rev, max_chars)
 
452
 
 
453
FORMATTERS = {'default': LongLogFormatter,
 
454
              'long': LongLogFormatter,
300
455
              'short': ShortLogFormatter,
 
456
              'line': LineLogFormatter,
301
457
              }
302
458
 
 
459
def register_formatter(name, formatter):
 
460
    FORMATTERS[name] = formatter
 
461
 
 
462
def set_default_formatter(formatter):
 
463
    FORMATTERS['default'] = formatter
 
464
 
303
465
 
304
466
def log_formatter(name, *args, **kwargs):
 
467
    """Construct a formatter from arguments.
 
468
 
 
469
    name -- Name of the formatter to construct; currently 'long', 'short' and
 
470
        'line' are supported.
 
471
    """
305
472
    from bzrlib.errors import BzrCommandError
306
 
    
307
473
    try:
308
474
        return FORMATTERS[name](*args, **kwargs)
309
475
    except IndexError:
310
476
        raise BzrCommandError("unknown log formatter: %r" % name)
 
477
 
 
478
def show_one_log(revno, rev, delta, verbose, to_file, show_timezone):
 
479
    # deprecated; for compatability
 
480
    lf = LongLogFormatter(to_file=to_file, show_timezone=show_timezone)
 
481
    lf.show(revno, rev, delta)
 
482
 
 
483
def show_changed_revisions(branch, old_rh, new_rh, to_file=None, log_format='long'):
 
484
    """Show the change in revision history comparing the old revision history to the new one.
 
485
 
 
486
    :param branch: The branch where the revisions exist
 
487
    :param old_rh: The old revision history
 
488
    :param new_rh: The new revision history
 
489
    :param to_file: A file to write the results to. If None, stdout will be used
 
490
    """
 
491
    if to_file is None:
 
492
        import sys
 
493
        import codecs
 
494
        import bzrlib
 
495
        to_file = codecs.getwriter(bzrlib.user_encoding)(sys.stdout, errors='replace')
 
496
    lf = log_formatter(log_format,
 
497
                       show_ids=False,
 
498
                       to_file=to_file,
 
499
                       show_timezone='original')
 
500
 
 
501
    # This is the first index which is different between
 
502
    # old and new
 
503
    base_idx = None
 
504
    for i in xrange(max(len(new_rh),
 
505
                        len(old_rh))):
 
506
        if (len(new_rh) <= i
 
507
            or len(old_rh) <= i
 
508
            or new_rh[i] != old_rh[i]):
 
509
            base_idx = i
 
510
            break
 
511
 
 
512
    if base_idx is None:
 
513
        to_file.write('Nothing seems to have changed\n')
 
514
        return
 
515
    ## TODO: It might be nice to do something like show_log
 
516
    ##       and show the merged entries. But since this is the
 
517
    ##       removed revisions, it shouldn't be as important
 
518
    if base_idx < len(old_rh):
 
519
        to_file.write('*'*60)
 
520
        to_file.write('\nRemoved Revisions:\n')
 
521
        for i in range(base_idx, len(old_rh)):
 
522
            rev = branch.repository.get_revision(old_rh[i])
 
523
            lf.show(i+1, rev, None)
 
524
        to_file.write('*'*60)
 
525
        to_file.write('\n\n')
 
526
    if base_idx < len(new_rh):
 
527
        to_file.write('Added Revisions:\n')
 
528
        show_log(branch,
 
529
                 lf,
 
530
                 None,
 
531
                 verbose=True,
 
532
                 direction='forward',
 
533
                 start_revision=base_idx+1,
 
534
                 end_revision=len(new_rh),
 
535
                 search=None)
 
536