~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/log.py

  • Committer: Martin Pool
  • Date: 2005-07-07 10:22:02 UTC
  • Revision ID: mbp@sourcefrog.net-20050707102201-2d2a13a25098b101
- rearrange and clear up merged weave

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
 
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.
 
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
50
36
"""
51
37
 
52
38
 
53
 
import bzrlib.errors as errors
54
 
from bzrlib.tree import EmptyTree
55
 
from bzrlib.delta import compare_trees
56
 
from bzrlib.trace import mutter
57
 
import re
58
 
 
 
39
from trace import mutter
59
40
 
60
41
def find_touching_revisions(branch, file_id):
61
42
    """Yield a description of revisions which affect the file_id.
102
83
 
103
84
 
104
85
 
105
 
def _enumerate_history(branch):
106
 
    rh = []
107
 
    revno = 1
108
 
    for rev_id in branch.revision_history():
109
 
        rh.append((revno, rev_id))
110
 
        revno += 1
111
 
    return rh
112
 
 
113
 
 
114
 
def _get_revision_delta(branch, revno):
115
 
    """Return the delta for a mainline revision.
116
 
    
117
 
    This is used to show summaries in verbose logs, and also for finding 
118
 
    revisions which touch a given file."""
119
 
    # XXX: What are we supposed to do when showing a summary for something 
120
 
    # other than a mainline revision.  The delta to it's first parent, or
121
 
    # (more useful) the delta to a nominated other revision.
122
 
    return branch.get_revision_delta(revno)
123
 
 
124
 
 
125
86
def show_log(branch,
126
87
             lf,
127
88
             specific_fileid=None,
128
89
             verbose=False,
129
90
             direction='reverse',
130
91
             start_revision=None,
131
 
             end_revision=None,
132
 
             search=None):
 
92
             end_revision=None):
133
93
    """Write out human-readable log of commits to this branch.
134
94
 
135
95
    lf
152
112
    end_revision
153
113
        If not None, only show revisions <= end_revision
154
114
    """
155
 
    branch.lock_read()
156
 
    try:
157
 
        _show_log(branch, lf, specific_fileid, verbose, direction,
158
 
                  start_revision, end_revision, search)
159
 
    finally:
160
 
        branch.unlock()
161
 
    
162
 
def _show_log(branch,
163
 
             lf,
164
 
             specific_fileid=None,
165
 
             verbose=False,
166
 
             direction='reverse',
167
 
             start_revision=None,
168
 
             end_revision=None,
169
 
             search=None):
170
 
    """Worker function for show_log - see show_log."""
171
115
    from bzrlib.osutils import format_date
172
116
    from bzrlib.errors import BzrCheckError
173
117
    from bzrlib.textui import show_status
180
124
    if specific_fileid:
181
125
        mutter('get log for file_id %r' % specific_fileid)
182
126
 
183
 
    if search is not None:
184
 
        import re
185
 
        searchRE = re.compile(search, re.IGNORECASE)
186
 
    else:
187
 
        searchRE = None
188
 
 
189
 
    which_revs = _enumerate_history(branch)
190
 
    
191
 
    if start_revision is None:
192
 
        start_revision = 1
193
 
    else:
194
 
        branch.check_real_revno(start_revision)
195
 
    
196
 
    if end_revision is None:
197
 
        end_revision = len(which_revs)
198
 
    else:
199
 
        branch.check_real_revno(end_revision)
200
 
 
201
 
    # list indexes are 0-based; revisions are 1-based
202
 
    cut_revs = which_revs[(start_revision-1):(end_revision)]
203
 
 
204
 
    if direction == 'reverse':
205
 
        cut_revs.reverse()
206
 
    elif direction == 'forward':
207
 
        pass
208
 
    else:
209
 
        raise ValueError('invalid direction %r' % direction)
210
 
 
211
 
    revision_history = branch.revision_history()
212
 
    for revno, rev_id in cut_revs:
213
 
        if verbose or specific_fileid:
214
 
            delta = _get_revision_delta(branch, revno)
215
 
            
 
127
    which_revs = branch.enum_history(direction)
 
128
    which_revs = [x for x in which_revs if (
 
129
            (start_revision is None or x[0] >= start_revision)
 
130
            and (end_revision is None or x[0] <= end_revision))]
 
131
 
 
132
    if not (verbose or specific_fileid):
 
133
        # no need to know what changed between revisions
 
134
        with_deltas = deltas_for_log_dummy(branch, which_revs)
 
135
    elif direction == 'reverse':
 
136
        with_deltas = deltas_for_log_reverse(branch, which_revs)
 
137
    else:        
 
138
        with_deltas = deltas_for_log_forward(branch, which_revs)
 
139
 
 
140
    for revno, rev, delta in with_deltas:
216
141
        if specific_fileid:
217
142
            if not delta.touches_file_id(specific_fileid):
218
143
                continue
221
146
            # although we calculated it, throw it away without display
222
147
            delta = None
223
148
 
224
 
        rev = branch.get_revision(rev_id)
225
 
 
226
 
        if searchRE:
227
 
            if not searchRE.search(rev.message):
228
 
                continue
229
 
 
230
149
        lf.show(revno, rev, delta)
231
 
        if hasattr(lf, 'show_merge'):
232
 
            if revno == 1:
233
 
                excludes = set()
234
 
            else:
235
 
                # revno is 1 based, so -2 to get back 1 less.
236
 
                excludes = set(branch.get_ancestry(revision_history[revno - 2]))
237
 
            pending = list(rev.parent_ids)
238
 
            while pending:
239
 
                rev_id = pending.pop()
240
 
                if rev_id in excludes:
241
 
                    continue
242
 
                # prevent showing merged revs twice if they multi-path.
243
 
                excludes.add(rev_id)
244
 
                try:
245
 
                    rev = branch.get_revision(rev_id)
246
 
                except errors.NoSuchRevision:
247
 
                    continue
248
 
                pending.extend(rev.parent_ids)
249
 
                lf.show_merge(rev)
 
150
 
250
151
 
251
152
 
252
153
def deltas_for_log_dummy(branch, which_revs):
253
 
    """Return all the revisions without intermediate deltas.
254
 
 
255
 
    Useful for log commands that won't need the delta information.
256
 
    """
257
 
    
258
154
    for revno, revision_id in which_revs:
259
155
        yield revno, branch.get_revision(revision_id), None
260
156
 
261
157
 
262
158
def deltas_for_log_reverse(branch, which_revs):
263
 
    """Compute deltas for display in latest-to-earliest order.
264
 
 
265
 
    branch
266
 
        Branch to traverse
267
 
 
268
 
    which_revs
269
 
        Sequence of (revno, revision_id) for the subset of history to examine
270
 
 
271
 
    returns 
272
 
        Sequence of (revno, rev, delta)
 
159
    """Compute deltas for display in reverse log.
 
160
 
 
161
    Given a sequence of (revno, revision_id) pairs, return
 
162
    (revno, rev, delta).
273
163
 
274
164
    The delta is from the given revision to the next one in the
275
165
    sequence, which makes sense if the log is being displayed from
276
166
    newest to oldest.
277
167
    """
 
168
    from tree import EmptyTree
 
169
    from diff import compare_trees
 
170
    
278
171
    last_revno = last_revision_id = last_tree = None
279
172
    for revno, revision_id in which_revs:
280
173
        this_tree = branch.revision_tree(revision_id)
283
176
        if last_revno:
284
177
            yield last_revno, last_revision, compare_trees(this_tree, last_tree, False)
285
178
 
286
 
        this_tree = EmptyTree(branch.get_root_id())
287
 
 
288
179
        last_revno = revno
289
180
        last_revision = this_revision
290
181
        last_tree = this_tree
291
182
 
292
183
    if last_revno:
293
184
        if last_revno == 1:
294
 
            this_tree = EmptyTree(branch.get_root_id())
 
185
            this_tree = EmptyTree()
295
186
        else:
296
187
            this_revno = last_revno - 1
297
188
            this_revision_id = branch.revision_history()[this_revno]
309
200
    sequence, which makes sense if the log is being displayed from
310
201
    newest to oldest.
311
202
    """
 
203
    from tree import EmptyTree
 
204
    from diff import compare_trees
 
205
 
312
206
    last_revno = last_revision_id = last_tree = None
313
 
    prev_tree = EmptyTree(branch.get_root_id())
314
 
 
315
207
    for revno, revision_id in which_revs:
316
208
        this_tree = branch.revision_tree(revision_id)
317
209
        this_revision = branch.get_revision(revision_id)
318
210
 
319
211
        if not last_revno:
320
212
            if revno == 1:
321
 
                last_tree = EmptyTree(branch.get_root_id())
 
213
                last_tree = EmptyTree()
322
214
            else:
323
215
                last_revno = revno - 1
324
216
                last_revision_id = branch.revision_history()[last_revno]
333
225
 
334
226
class LogFormatter(object):
335
227
    """Abstract class to display log messages."""
336
 
    def __init__(self, to_file, show_ids=False, show_timezone='original'):
 
228
    def __init__(self, to_file, show_ids=False, show_timezone=False):
337
229
        self.to_file = to_file
338
230
        self.show_ids = show_ids
339
231
        self.show_timezone = show_timezone
340
 
 
341
 
 
342
 
    def show(self, revno, rev, delta):
343
 
        raise NotImplementedError('not implemented in abstract base')
344
 
 
345
 
    
 
232
        
 
233
 
 
234
 
 
235
 
 
236
 
 
237
 
346
238
class LongLogFormatter(LogFormatter):
347
239
    def show(self, revno, rev, delta):
348
240
        from osutils import format_date
353
245
        print >>to_file,  'revno:', revno
354
246
        if self.show_ids:
355
247
            print >>to_file,  'revision-id:', rev.revision_id
356
 
 
357
 
            for parent_id in rev.parent_ids:
358
 
                print >>to_file, 'parent:', parent_id
359
 
            
360
248
        print >>to_file,  'committer:', rev.committer
361
 
 
362
 
        date_str = format_date(rev.timestamp,
363
 
                               rev.timezone or 0,
364
 
                               self.show_timezone)
365
 
        print >>to_file,  'timestamp: %s' % date_str
 
249
        print >>to_file,  'timestamp: %s' % (format_date(rev.timestamp, rev.timezone or 0,
 
250
                                             self.show_timezone))
366
251
 
367
252
        print >>to_file,  'message:'
368
253
        if not rev.message:
374
259
        if delta != None:
375
260
            delta.show(to_file, self.show_ids)
376
261
 
377
 
    def show_merge(self, rev):
378
 
        from osutils import format_date
379
 
 
380
 
        to_file = self.to_file
381
 
 
382
 
        indent = '    '
383
 
 
384
 
        print >>to_file,  indent+'-' * 60
385
 
        print >>to_file,  indent+'merged:', rev.revision_id
386
 
        if self.show_ids:
387
 
            for parent_id in rev.parent_ids:
388
 
                print >>to_file, indent+'parent:', parent_id
389
 
            
390
 
        print >>to_file,  indent+'committer:', rev.committer
391
 
 
392
 
        date_str = format_date(rev.timestamp,
393
 
                               rev.timezone or 0,
394
 
                               self.show_timezone)
395
 
        print >>to_file,  indent+'timestamp: %s' % date_str
396
 
 
397
 
        print >>to_file,  indent+'message:'
398
 
        if not rev.message:
399
 
            print >>to_file,  indent+'  (no message)'
400
 
        else:
401
 
            for l in rev.message.split('\n'):
402
 
                print >>to_file,  indent+'  ' + l
403
262
 
404
263
 
405
264
class ShortLogFormatter(LogFormatter):
407
266
        from bzrlib.osutils import format_date
408
267
 
409
268
        to_file = self.to_file
410
 
        date_str = format_date(rev.timestamp, rev.timezone or 0,
411
 
                            self.show_timezone)
 
269
 
412
270
        print >>to_file, "%5d %s\t%s" % (revno, rev.committer,
413
271
                format_date(rev.timestamp, rev.timezone or 0,
414
272
                            self.show_timezone))
420
278
            for l in rev.message.split('\n'):
421
279
                print >>to_file,  '      ' + l
422
280
 
423
 
        # TODO: Why not show the modified files in a shorter form as
424
 
        # well? rewrap them single lines of appropriate length
425
281
        if delta != None:
426
282
            delta.show(to_file, self.show_ids)
427
283
        print
428
284
 
429
 
class LineLogFormatter(LogFormatter):
430
 
    def truncate(self, str, max_len):
431
 
        if len(str) <= max_len:
432
 
            return str
433
 
        return str[:max_len-3]+'...'
434
 
 
435
 
    def date_string(self, rev):
436
 
        from bzrlib.osutils import format_date
437
 
        return format_date(rev.timestamp, rev.timezone or 0, 
438
 
                           self.show_timezone, date_fmt="%Y-%m-%d",
439
 
                           show_offset=False)
440
 
 
441
 
    def message(self, rev):
442
 
        if not rev.message:
443
 
            return '(no message)'
444
 
        else:
445
 
            return rev.message
446
 
 
447
 
    def short_committer(self, rev):
448
 
        return re.sub('<.*@.*>', '', rev.committer).strip(' ')
449
 
    
450
 
    def show(self, revno, rev, delta):
451
 
        print >> self.to_file, self.log_string(rev, 79) 
452
 
 
453
 
    def log_string(self, rev, max_chars):
454
 
        out = [self.truncate(self.short_committer(rev), 20)]
455
 
        out.append(self.date_string(rev))
456
 
        out.append(self.message(rev).replace('\n', ' '))
457
 
        return self.truncate(" ".join(out).rstrip('\n'), max_chars)
458
 
 
459
 
def line_log(rev, max_chars):
460
 
    lf = LineLogFormatter(None)
461
 
    return lf.log_string(rev, max_chars)
 
285
 
462
286
 
463
287
FORMATTERS = {'long': LongLogFormatter,
464
288
              'short': ShortLogFormatter,
465
 
              'line': LineLogFormatter,
466
289
              }
467
290
 
468
291
 
469
292
def log_formatter(name, *args, **kwargs):
470
 
    """Construct a formatter from arguments.
471
 
 
472
 
    name -- Name of the formatter to construct; currently 'long', 'short' and
473
 
        'line' are supported.
474
 
    """
475
293
    from bzrlib.errors import BzrCommandError
 
294
    
476
295
    try:
477
296
        return FORMATTERS[name](*args, **kwargs)
478
297
    except IndexError:
479
298
        raise BzrCommandError("unknown log formatter: %r" % name)
480
 
 
481
 
def show_one_log(revno, rev, delta, verbose, to_file, show_timezone):
482
 
    # deprecated; for compatability
483
 
    lf = LongLogFormatter(to_file=to_file, show_timezone=show_timezone)
484
 
    lf.show(revno, rev, delta)