~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/log.py

  • Committer: Martin Pool
  • Date: 2006-06-05 18:00:36 UTC
  • mto: This revision was merged to the branch mainline in revision 1752.
  • Revision ID: mbp@sourcefrog.net-20060605180036-04f5d0cea94ed999
clean up test kipple

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