~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/log.py

MergeĀ fromĀ jam-storage.

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# Copyright (C) 2005 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 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.
 
50
"""
 
51
 
 
52
 
 
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
 
 
61
 
 
62
def find_touching_revisions(branch, file_id):
 
63
    """Yield a description of revisions which affect the file_id.
 
64
 
 
65
    Each returned element is (revno, revision_id, description)
 
66
 
 
67
    This is the list of revisions where the file is either added,
 
68
    modified, renamed or deleted.
 
69
 
 
70
    TODO: Perhaps some way to limit this to only particular revisions,
 
71
    or to traverse a non-mainline set of revisions?
 
72
    """
 
73
    last_ie = None
 
74
    last_path = None
 
75
    revno = 1
 
76
    for revision_id in branch.revision_history():
 
77
        this_inv = branch.repository.get_revision_inventory(revision_id)
 
78
        if file_id in this_inv:
 
79
            this_ie = this_inv[file_id]
 
80
            this_path = this_inv.id2path(file_id)
 
81
        else:
 
82
            this_ie = this_path = None
 
83
 
 
84
        # now we know how it was last time, and how it is in this revision.
 
85
        # are those two states effectively the same or not?
 
86
 
 
87
        if not this_ie and not last_ie:
 
88
            # not present in either
 
89
            pass
 
90
        elif this_ie and not last_ie:
 
91
            yield revno, revision_id, "added " + this_path
 
92
        elif not this_ie and last_ie:
 
93
            # deleted here
 
94
            yield revno, revision_id, "deleted " + last_path
 
95
        elif this_path != last_path:
 
96
            yield revno, revision_id, ("renamed %s => %s" % (last_path, this_path))
 
97
        elif (this_ie.text_size != last_ie.text_size
 
98
              or this_ie.text_sha1 != last_ie.text_sha1):
 
99
            yield revno, revision_id, "modified " + this_path
 
100
 
 
101
        last_ie = this_ie
 
102
        last_path = this_path
 
103
        revno += 1
 
104
 
 
105
 
 
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
 
 
127
def show_log(branch,
 
128
             lf,
 
129
             specific_fileid=None,
 
130
             verbose=False,
 
131
             direction='reverse',
 
132
             start_revision=None,
 
133
             end_revision=None,
 
134
             search=None):
 
135
    """Write out human-readable log of commits to this branch.
 
136
 
 
137
    lf
 
138
        LogFormatter object to show the output.
 
139
 
 
140
    specific_fileid
 
141
        If true, list only the commits affecting the specified
 
142
        file, rather than all commits.
 
143
 
 
144
    verbose
 
145
        If true show added/changed/deleted/renamed files.
 
146
 
 
147
    direction
 
148
        'reverse' (default) is latest to earliest;
 
149
        'forward' is earliest to latest.
 
150
 
 
151
    start_revision
 
152
        If not None, only show revisions >= start_revision
 
153
 
 
154
    end_revision
 
155
        If not None, only show revisions <= end_revision
 
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."""
 
173
    from bzrlib.osutils import format_date
 
174
    from bzrlib.errors import BzrCheckError
 
175
    from bzrlib.textui import show_status
 
176
    
 
177
    from warnings import warn
 
178
 
 
179
    if not isinstance(lf, LogFormatter):
 
180
        warn("not a LogFormatter instance: %r" % lf)
 
181
 
 
182
    if specific_fileid:
 
183
        mutter('get log for file_id %r', specific_fileid)
 
184
 
 
185
    if search is not None:
 
186
        import re
 
187
        searchRE = re.compile(search, re.IGNORECASE)
 
188
    else:
 
189
        searchRE = None
 
190
 
 
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
            
 
218
        if specific_fileid:
 
219
            if not delta.touches_file_id(specific_fileid):
 
220
                continue
 
221
 
 
222
        if not verbose:
 
223
            # although we calculated it, throw it away without display
 
224
            delta = None
 
225
 
 
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)
 
254
 
 
255
 
 
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
    
 
262
    for revno, revision_id in which_revs:
 
263
        yield revno, branch.get_revision(revision_id), None
 
264
 
 
265
 
 
266
def deltas_for_log_reverse(branch, which_revs):
 
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)
 
277
 
 
278
    The delta is from the given revision to the next one in the
 
279
    sequence, which makes sense if the log is being displayed from
 
280
    newest to oldest.
 
281
    """
 
282
    last_revno = last_revision_id = last_tree = None
 
283
    for revno, revision_id in which_revs:
 
284
        this_tree = branch.revision_tree(revision_id)
 
285
        this_revision = branch.get_revision(revision_id)
 
286
        
 
287
        if last_revno:
 
288
            yield last_revno, last_revision, compare_trees(this_tree, last_tree, False)
 
289
 
 
290
        this_tree = EmptyTree(branch.get_root_id())
 
291
 
 
292
        last_revno = revno
 
293
        last_revision = this_revision
 
294
        last_tree = this_tree
 
295
 
 
296
    if last_revno:
 
297
        if last_revno == 1:
 
298
            this_tree = EmptyTree(branch.get_root_id())
 
299
        else:
 
300
            this_revno = last_revno - 1
 
301
            this_revision_id = branch.revision_history()[this_revno]
 
302
            this_tree = branch.revision_tree(this_revision_id)
 
303
        yield last_revno, last_revision, compare_trees(this_tree, last_tree, False)
 
304
 
 
305
 
 
306
def deltas_for_log_forward(branch, which_revs):
 
307
    """Compute deltas for display in forward log.
 
308
 
 
309
    Given a sequence of (revno, revision_id) pairs, return
 
310
    (revno, rev, delta).
 
311
 
 
312
    The delta is from the given revision to the next one in the
 
313
    sequence, which makes sense if the log is being displayed from
 
314
    newest to oldest.
 
315
    """
 
316
    last_revno = last_revision_id = last_tree = None
 
317
    prev_tree = EmptyTree(branch.get_root_id())
 
318
 
 
319
    for revno, revision_id in which_revs:
 
320
        this_tree = branch.revision_tree(revision_id)
 
321
        this_revision = branch.get_revision(revision_id)
 
322
 
 
323
        if not last_revno:
 
324
            if revno == 1:
 
325
                last_tree = EmptyTree(branch.get_root_id())
 
326
            else:
 
327
                last_revno = revno - 1
 
328
                last_revision_id = branch.revision_history()[last_revno]
 
329
                last_tree = branch.revision_tree(last_revision_id)
 
330
 
 
331
        yield revno, this_revision, compare_trees(last_tree, this_tree, False)
 
332
 
 
333
        last_revno = revno
 
334
        last_revision = this_revision
 
335
        last_tree = this_tree
 
336
 
 
337
 
 
338
class LogFormatter(object):
 
339
    """Abstract class to display log messages."""
 
340
    def __init__(self, to_file, show_ids=False, show_timezone='original'):
 
341
        self.to_file = to_file
 
342
        self.show_ids = show_ids
 
343
        self.show_timezone = show_timezone
 
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
    
 
353
class LongLogFormatter(LogFormatter):
 
354
    def show(self, revno, rev, delta):
 
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
 
363
        to_file = self.to_file
 
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
 
371
        if self.show_ids:
 
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
 
384
 
 
385
        print >>to_file,  indent+'message:'
 
386
        if not rev.message:
 
387
            print >>to_file,  indent+'  (no message)'
 
388
        else:
 
389
            message = rev.message.rstrip('\r\n')
 
390
            for l in message.split('\n'):
 
391
                print >>to_file,  indent+'  ' + l
 
392
        if delta != None:
 
393
            delta.show(to_file, self.show_ids)
 
394
 
 
395
 
 
396
class ShortLogFormatter(LogFormatter):
 
397
    def show(self, revno, rev, delta):
 
398
        from bzrlib.osutils import format_date
 
399
 
 
400
        to_file = self.to_file
 
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),
 
404
                format_date(rev.timestamp, rev.timezone or 0,
 
405
                            self.show_timezone, date_fmt="%Y-%m-%d",
 
406
                           show_offset=False))
 
407
        if self.show_ids:
 
408
            print >>to_file,  '      revision-id:', rev.revision_id
 
409
        if not rev.message:
 
410
            print >>to_file,  '      (no message)'
 
411
        else:
 
412
            message = rev.message.rstrip('\r\n')
 
413
            for l in message.split('\n'):
 
414
                print >>to_file,  '      ' + l
 
415
 
 
416
        # TODO: Why not show the modified files in a shorter form as
 
417
        # well? rewrap them single lines of appropriate length
 
418
        if delta != None:
 
419
            delta.show(to_file, self.show_ids)
 
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 = {'long': LongLogFormatter,
 
454
              'short': ShortLogFormatter,
 
455
              'line': LineLogFormatter,
 
456
              }
 
457
 
 
458
 
 
459
def log_formatter(name, *args, **kwargs):
 
460
    """Construct a formatter from arguments.
 
461
 
 
462
    name -- Name of the formatter to construct; currently 'long', 'short' and
 
463
        'line' are supported.
 
464
    """
 
465
    from bzrlib.errors import BzrCommandError
 
466
    try:
 
467
        return FORMATTERS[name](*args, **kwargs)
 
468
    except IndexError:
 
469
        raise BzrCommandError("unknown log formatter: %r" % name)
 
470
 
 
471
def show_one_log(revno, rev, delta, verbose, to_file, show_timezone):
 
472
    # deprecated; for compatability
 
473
    lf = LongLogFormatter(to_file=to_file, show_timezone=show_timezone)
 
474
    lf.show(revno, rev, delta)
 
475
 
 
476
def show_changed_revisions(branch, old_rh, new_rh, to_file=None, log_format='long'):
 
477
    """Show the change in revision history comparing the old revision history to the new one.
 
478
 
 
479
    :param branch: The branch where the revisions exist
 
480
    :param old_rh: The old revision history
 
481
    :param new_rh: The new revision history
 
482
    :param to_file: A file to write the results to. If None, stdout will be used
 
483
    """
 
484
    if to_file is None:
 
485
        import sys
 
486
        import codecs
 
487
        import bzrlib
 
488
        to_file = codecs.getwriter(bzrlib.user_encoding)(sys.stdout, errors='replace')
 
489
    lf = log_formatter(log_format,
 
490
                       show_ids=False,
 
491
                       to_file=to_file,
 
492
                       show_timezone='original')
 
493
 
 
494
    # This is the first index which is different between
 
495
    # old and new
 
496
    base_idx = None
 
497
    for i in xrange(max(len(new_rh),
 
498
                        len(old_rh))):
 
499
        if (len(new_rh) <= i
 
500
            or len(old_rh) <= i
 
501
            or new_rh[i] != old_rh[i]):
 
502
            base_idx = i
 
503
            break
 
504
 
 
505
    if base_idx is None:
 
506
        to_file.write('Nothing seems to have changed\n')
 
507
        return
 
508
    ## TODO: It might be nice to do something like show_log
 
509
    ##       and show the merged entries. But since this is the
 
510
    ##       removed revisions, it shouldn't be as important
 
511
    if base_idx < len(old_rh):
 
512
        to_file.write('*'*60)
 
513
        to_file.write('\nRemoved Revisions:\n')
 
514
        for i in range(base_idx, len(old_rh)):
 
515
            rev = branch.repository.get_revision(old_rh[i])
 
516
            lf.show(i+1, rev, None)
 
517
        to_file.write('*'*60)
 
518
        to_file.write('\n\n')
 
519
    if base_idx < len(new_rh):
 
520
        to_file.write('Added Revisions:\n')
 
521
        show_log(branch,
 
522
                 lf,
 
523
                 None,
 
524
                 verbose=True,
 
525
                 direction='forward',
 
526
                 start_revision=base_idx+1,
 
527
                 end_revision=len(new_rh),
 
528
                 search=None)
 
529